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 | # Sanic | ||||||
|  |  | ||||||
|  | [](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||||||
|  |  | ||||||
| [](https://travis-ci.org/channelcat/sanic) | [](https://travis-ci.org/channelcat/sanic) | ||||||
| [](https://pypi.python.org/pypi/sanic/) | [](https://pypi.python.org/pypi/sanic/) | ||||||
| [](https://pypi.python.org/pypi/sanic/) | [](https://pypi.python.org/pypi/sanic/) | ||||||
| @@ -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 | | | 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 | | | Wheezy  | gunicorn + meinheld |       20,244 |      4.94ms | | ||||||
| | Falcon  | gunicorn + meinheld |       18,972 |      5.27ms | | | Falcon  | gunicorn + meinheld |       18,972 |      5.27ms | | ||||||
| | Bottle  | gunicorn + meinheld |       13,596 |      7.36ms | | | Bottle  | gunicorn + meinheld |       13,596 |      7.36ms | | ||||||
| | Flask   | gunicorn + meinheld |        4,988 |     20.08ms | | | Flask   | gunicorn + meinheld |        4,988 |     20.08ms | | ||||||
| | Kyoukai | Python 3.5 + uvloop |        3,889 |     27.44ms | | | Kyoukai | Python 3.5 + uvloop |        3,889 |     27.44ms | | ||||||
| | Aiohttp | Python 3.5 + uvloop |        2,979 |     33.42ms | | | Aiohttp | Python 3.5 + uvloop |        2,979 |     33.42ms | | ||||||
|  | | Tornado | Python 3.5          |        2,138 |     46.66ms | | ||||||
|  |  | ||||||
| ## Hello World | ## 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 import Sanic | ||||||
| from sanic.response import json | from sanic.response import json | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic() | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
| async def test(request): | async def test(request): | ||||||
|     return json({ "hello": "world" }) |     return json({"hello": "world"}) | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000) | 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) |  * [Middleware](docs/middleware.md) | ||||||
|  * [Exceptions](docs/exceptions.md) |  * [Exceptions](docs/exceptions.md) | ||||||
|  * [Blueprints](docs/blueprints.md) |  * [Blueprints](docs/blueprints.md) | ||||||
|  |  * [Class Based Views](docs/class_based_views.md) | ||||||
|  |  * [Cookies](docs/cookies.md) | ||||||
|  |  * [Static Files](docs/static_files.md) | ||||||
|  * [Deploying](docs/deploying.md) |  * [Deploying](docs/deploying.md) | ||||||
|  * [Contributing](docs/contributing.md) |  * [Contributing](docs/contributing.md) | ||||||
|  * [License](LICENSE) |  * [License](LICENSE) | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ from sanic import Blueprint | |||||||
| bp = Blueprint('my_blueprint') | bp = Blueprint('my_blueprint') | ||||||
|  |  | ||||||
| @bp.route('/') | @bp.route('/') | ||||||
| async def bp_root(): | async def bp_root(request): | ||||||
|     return json({'my': 'blueprint'}) |     return json({'my': 'blueprint'}) | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| @@ -42,7 +42,7 @@ from sanic import Sanic | |||||||
| from my_blueprint import bp | from my_blueprint import bp | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic(__name__) | ||||||
| app.register_blueprint(bp) | app.blueprint(bp) | ||||||
|  |  | ||||||
| app.run(host='0.0.0.0', port=8000, debug=True) | 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) | @bp.exception(NotFound) | ||||||
| def ignore_404s(request, exception): | def ignore_404s(request, exception): | ||||||
| 	return text("Yep, I totally found the page: {}".format(request.url)) | 	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.json` (any) - JSON body   | ||||||
| `request.args` (dict) - Query String variables.  Use getlist to get multiple of the same name   | `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.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 | See request.py for more information | ||||||
|  |  | ||||||
| @@ -15,7 +16,7 @@ See request.py for more information | |||||||
|  |  | ||||||
| ```python | ```python | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic.response import json, text | ||||||
|  |  | ||||||
| @app.route("/json") | @app.route("/json") | ||||||
| def post_json(request): | def post_json(request): | ||||||
| @@ -40,4 +41,9 @@ def post_json(request): | |||||||
| @app.route("/query_string") | @app.route("/query_string") | ||||||
| def query_string(request): | def query_string(request): | ||||||
|     return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) |     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 | from sanic.response import text | ||||||
|  |  | ||||||
| @app.route('/tag/<tag>') | @app.route('/tag/<tag>') | ||||||
| async def person_handler(request, tag): | async def tag_handler(request, tag): | ||||||
| 	return text('Tag - {}'.format(tag)) | 	return text('Tag - {}'.format(tag)) | ||||||
|  |  | ||||||
| @app.route('/number/<integer_arg:int>') | @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)) | 	return text('Integer - {}'.format(integer_arg)) | ||||||
|  |  | ||||||
| @app.route('/number/<number_arg:number>') | @app.route('/number/<number_arg:number>') | ||||||
| async def person_handler(request, number_arg): | async def number_handler(request, number_arg): | ||||||
| 	return text('Number - {}'.format(number)) | 	return text('Number - {}'.format(number_arg)) | ||||||
|  |  | ||||||
| @app.route('/person/<name:[A-z]>') | @app.route('/person/<name:[A-z]>') | ||||||
| async def person_handler(request, name): | async def person_handler(request, name): | ||||||
| @@ -29,4 +29,16 @@ async def person_handler(request, name): | |||||||
| async def folder_handler(request, folder_id): | async def folder_handler(request, folder_id): | ||||||
| 	return text('Folder - {}'.format(folder_id)) | 	return text('Folder - {}'.format(folder_id)) | ||||||
|  |  | ||||||
|  | async def handler1(request): | ||||||
|  | 	return text('OK') | ||||||
|  | app.add_route(handler1, '/test') | ||||||
|  |  | ||||||
|  | async def 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 | ujson | ||||||
| uvloop | uvloop | ||||||
| aiohttp | aiohttp | ||||||
|  | aiocache | ||||||
| pytest | pytest | ||||||
| coverage | coverage | ||||||
| tox | tox | ||||||
| @@ -9,3 +10,5 @@ gunicorn | |||||||
| bottle | bottle | ||||||
| kyoukai | kyoukai | ||||||
| falcon | falcon | ||||||
|  | tornado | ||||||
|  | aiofiles | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
| httptools | httptools | ||||||
| ujson | ujson | ||||||
| uvloop | uvloop | ||||||
|  | aiofiles | ||||||
|  | multidict | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
| from .sanic import Sanic | from .sanic import Sanic | ||||||
| from .blueprints import Blueprint | from .blueprints import Blueprint | ||||||
|  |  | ||||||
|  | __version__ = '0.1.8' | ||||||
|  |  | ||||||
| __all__ = ['Sanic', 'Blueprint'] | __all__ = ['Sanic', 'Blueprint'] | ||||||
|   | |||||||
| @@ -1,3 +1,6 @@ | |||||||
|  | from collections import defaultdict | ||||||
|  |  | ||||||
|  |  | ||||||
| class BlueprintSetup: | class BlueprintSetup: | ||||||
|     """ |     """ | ||||||
|     """ |     """ | ||||||
| @@ -22,7 +25,7 @@ class BlueprintSetup: | |||||||
|         if self.url_prefix: |         if self.url_prefix: | ||||||
|             uri = self.url_prefix + uri |             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): |     def add_exception(self, handler, *args, **kwargs): | ||||||
|         """ |         """ | ||||||
| @@ -30,6 +33,15 @@ class BlueprintSetup: | |||||||
|         """ |         """ | ||||||
|         self.app.exception(*args, **kwargs)(handler) |         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): |     def add_middleware(self, middleware, *args, **kwargs): | ||||||
|         """ |         """ | ||||||
|         Registers middleware to sanic |         Registers middleware to sanic | ||||||
| @@ -42,9 +54,15 @@ class BlueprintSetup: | |||||||
|  |  | ||||||
| class Blueprint: | class Blueprint: | ||||||
|     def __init__(self, name, url_prefix=None): |     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.name = name | ||||||
|         self.url_prefix = url_prefix |         self.url_prefix = url_prefix | ||||||
|         self.deferred_functions = [] |         self.deferred_functions = [] | ||||||
|  |         self.listeners = defaultdict(list) | ||||||
|  |  | ||||||
|     def record(self, func): |     def record(self, func): | ||||||
|         """ |         """ | ||||||
| @@ -73,6 +91,20 @@ class Blueprint: | |||||||
|             return handler |             return handler | ||||||
|         return decorator |         return decorator | ||||||
|  |  | ||||||
|  |     def add_route(self, handler, uri, methods=None): | ||||||
|  |         """ | ||||||
|  |         """ | ||||||
|  |         self.record(lambda s: s.add_route(handler, uri, methods)) | ||||||
|  |         return handler | ||||||
|  |  | ||||||
|  |     def listener(self, event): | ||||||
|  |         """ | ||||||
|  |         """ | ||||||
|  |         def decorator(listener): | ||||||
|  |             self.listeners[event].append(listener) | ||||||
|  |             return listener | ||||||
|  |         return decorator | ||||||
|  |  | ||||||
|     def middleware(self, *args, **kwargs): |     def middleware(self, *args, **kwargs): | ||||||
|         """ |         """ | ||||||
|         """ |         """ | ||||||
| @@ -83,8 +115,9 @@ class Blueprint: | |||||||
|  |  | ||||||
|         # Detect which way this was called, @middleware or @middleware('AT') |         # Detect which way this was called, @middleware or @middleware('AT') | ||||||
|         if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): |         if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): | ||||||
|  |             middleware = args[0] | ||||||
|             args = [] |             args = [] | ||||||
|             return register_middleware(args[0]) |             return register_middleware(middleware) | ||||||
|         else: |         else: | ||||||
|             return register_middleware |             return register_middleware | ||||||
|  |  | ||||||
| @@ -95,3 +128,9 @@ class Blueprint: | |||||||
|             self.record(lambda s: s.add_exception(handler, *args, **kwargs)) |             self.record(lambda s: s.add_exception(handler, *args, **kwargs)) | ||||||
|             return handler |             return handler | ||||||
|         return decorator |         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_MAX_SIZE = 100000000  # 100 megababies | ||||||
|     REQUEST_TIMEOUT = 60  # 60 seconds |     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 |     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: | class Handler: | ||||||
|     handlers = None |     handlers = None | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| from cgi import parse_header | from cgi import parse_header | ||||||
| from collections import namedtuple | from collections import namedtuple | ||||||
|  | from http.cookies import SimpleCookie | ||||||
| from httptools import parse_url | from httptools import parse_url | ||||||
| from urllib.parse import parse_qs | from urllib.parse import parse_qs | ||||||
| from ujson import loads as json_loads | from ujson import loads as json_loads | ||||||
| @@ -7,6 +8,12 @@ from ujson import loads as json_loads | |||||||
| from .log import log | from .log import log | ||||||
|  |  | ||||||
|  |  | ||||||
|  | DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" | ||||||
|  | # HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 | ||||||
|  | # > If the media type remains unknown, the recipient SHOULD treat it | ||||||
|  | # > as type "application/octet-stream" | ||||||
|  |  | ||||||
|  |  | ||||||
| class RequestParameters(dict): | class RequestParameters(dict): | ||||||
|     """ |     """ | ||||||
|     Hosts a dict with lists as values where get returns the first |     Hosts a dict with lists as values where get returns the first | ||||||
| @@ -25,12 +32,12 @@ class RequestParameters(dict): | |||||||
|         return self.super.get(name, default) |         return self.super.get(name, default) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Request: | class Request(dict): | ||||||
|     """ |     """ | ||||||
|     Properties of an HTTP request such as URL, headers, etc. |     Properties of an HTTP request such as URL, headers, etc. | ||||||
|     """ |     """ | ||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         'url', 'headers', 'version', 'method', |         'url', 'headers', 'version', 'method', '_cookies', | ||||||
|         'query_string', 'body', |         'query_string', 'body', | ||||||
|         'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', |         'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', | ||||||
|     ) |     ) | ||||||
| @@ -52,6 +59,7 @@ class Request: | |||||||
|         self.parsed_form = None |         self.parsed_form = None | ||||||
|         self.parsed_files = None |         self.parsed_files = None | ||||||
|         self.parsed_args = None |         self.parsed_args = None | ||||||
|  |         self._cookies = None | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def json(self): |     def json(self): | ||||||
| @@ -59,21 +67,20 @@ class Request: | |||||||
|             try: |             try: | ||||||
|                 self.parsed_json = json_loads(self.body) |                 self.parsed_json = json_loads(self.body) | ||||||
|             except Exception: |             except Exception: | ||||||
|                 pass |                 log.exception("failed when parsing body as json") | ||||||
|  |  | ||||||
|         return self.parsed_json |         return self.parsed_json | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def form(self): |     def form(self): | ||||||
|         if self.parsed_form is None: |         if self.parsed_form is None: | ||||||
|             self.parsed_form = {} |             self.parsed_form = RequestParameters() | ||||||
|             self.parsed_files = {} |             self.parsed_files = RequestParameters() | ||||||
|             content_type, parameters = parse_header( |             content_type = self.headers.get( | ||||||
|                 self.headers.get('Content-Type')) |                 'Content-Type', DEFAULT_HTTP_CONTENT_TYPE) | ||||||
|  |             content_type, parameters = parse_header(content_type) | ||||||
|             try: |             try: | ||||||
|                 is_url_encoded = ( |                 if content_type == 'application/x-www-form-urlencoded': | ||||||
|                     content_type == 'application/x-www-form-urlencoded') |  | ||||||
|                 if content_type is None or is_url_encoded: |  | ||||||
|                     self.parsed_form = RequestParameters( |                     self.parsed_form = RequestParameters( | ||||||
|                         parse_qs(self.body.decode('utf-8'))) |                         parse_qs(self.body.decode('utf-8'))) | ||||||
|                 elif content_type == 'multipart/form-data': |                 elif content_type == 'multipart/form-data': | ||||||
| @@ -81,9 +88,8 @@ class Request: | |||||||
|                     boundary = parameters['boundary'].encode('utf-8') |                     boundary = parameters['boundary'].encode('utf-8') | ||||||
|                     self.parsed_form, self.parsed_files = ( |                     self.parsed_form, self.parsed_files = ( | ||||||
|                         parse_multipart_form(self.body, boundary)) |                         parse_multipart_form(self.body, boundary)) | ||||||
|             except Exception as e: |             except Exception: | ||||||
|                 log.exception(e) |                 log.exception("failed when parsing form") | ||||||
|                 pass |  | ||||||
|  |  | ||||||
|         return self.parsed_form |         return self.parsed_form | ||||||
|  |  | ||||||
| @@ -105,6 +111,18 @@ class Request: | |||||||
|  |  | ||||||
|         return self.parsed_args |         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']) | 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 |     Parses a request body and returns fields and files | ||||||
|     :param body: Bytes request body |     :param body: Bytes request body | ||||||
|     :param boundary: Bytes multipart boundary |     :param boundary: Bytes multipart boundary | ||||||
|     :return: fields (dict), files (dict) |     :return: fields (RequestParameters), files (RequestParameters) | ||||||
|     """ |     """ | ||||||
|     files = {} |     files = RequestParameters() | ||||||
|     fields = {} |     fields = RequestParameters() | ||||||
|  |  | ||||||
|     form_parts = body.split(boundary) |     form_parts = body.split(boundary) | ||||||
|     for form_part in form_parts[1:-1]: |     for form_part in form_parts[1:-1]: | ||||||
| @@ -148,9 +166,16 @@ def parse_multipart_form(body, boundary): | |||||||
|  |  | ||||||
|         post_data = form_part[line_index:-4] |         post_data = form_part[line_index:-4] | ||||||
|         if file_name or file_type: |         if file_name or file_type: | ||||||
|             files[field_name] = File( |             file = File(type=file_type, name=file_name, body=post_data) | ||||||
|                 type=file_type, name=file_name, body=post_data) |             if field_name in files: | ||||||
|  |                 files[field_name].append(file) | ||||||
|             else: |             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 |     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', |     200: b'OK', | ||||||
|     400: b'Bad Request', |     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', |     401: b'Unauthorized', | ||||||
|     402: b'Payment Required', |     402: b'Payment Required', | ||||||
|     403: b'Forbidden', |     403: b'Forbidden', | ||||||
|     404: b'Not Found', |     404: b'Not Found', | ||||||
|     405: b'Method Not Allowed', |     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', |     500: b'Internal Server Error', | ||||||
|     501: b'Not Implemented', |     501: b'Not Implemented', | ||||||
|     502: b'Bad Gateway', |     502: b'Bad Gateway', | ||||||
|     503: b'Service Unavailable', |     503: b'Service Unavailable', | ||||||
|     504: b'Gateway Timeout', |     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: | class HTTPResponse: | ||||||
|     __slots__ = ('body', 'status', 'content_type', 'headers') |     __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') | ||||||
|  |  | ||||||
|     def __init__(self, body=None, status=200, headers=None, |     def __init__(self, body=None, status=200, headers=None, | ||||||
|                  content_type='text/plain', body_bytes=b''): |                  content_type='text/plain', body_bytes=b''): | ||||||
| @@ -30,6 +85,7 @@ class HTTPResponse: | |||||||
|  |  | ||||||
|         self.status = status |         self.status = status | ||||||
|         self.headers = headers or {} |         self.headers = headers or {} | ||||||
|  |         self._cookies = None | ||||||
|  |  | ||||||
|     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): |     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||||
|         # This is all returned in a kind-of funky way |         # 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')) |                 b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) | ||||||
|                 for name, value in self.headers.items() |                 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' |         return (b'HTTP/%b %d %b\r\n' | ||||||
|                 b'Content-Type: %b\r\n' |                 b'Content-Type: %b\r\n' | ||||||
|                 b'Content-Length: %d\r\n' |                 b'Content-Length: %d\r\n' | ||||||
| @@ -52,7 +115,7 @@ class HTTPResponse: | |||||||
|                 b'%b') % ( |                 b'%b') % ( | ||||||
|             version.encode(), |             version.encode(), | ||||||
|             self.status, |             self.status, | ||||||
|             STATUS_CODES.get(self.status, b'FAIL'), |             status, | ||||||
|             self.content_type.encode(), |             self.content_type.encode(), | ||||||
|             len(self.body), |             len(self.body), | ||||||
|             b'keep-alive' if keep_alive else b'close', |             b'keep-alive' if keep_alive else b'close', | ||||||
| @@ -61,10 +124,16 @@ class HTTPResponse: | |||||||
|             self.body |             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): | def json(body, status=200, headers=None): | ||||||
|     return HTTPResponse(ujson.dumps(body), headers=headers, status=status, |     return HTTPResponse(json_dumps(body), headers=headers, status=status, | ||||||
|                         content_type="application/json; charset=utf-8") |                         content_type="application/json") | ||||||
|  |  | ||||||
|  |  | ||||||
| def text(body, status=200, headers=None): | 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): | def html(body, status=200, headers=None): | ||||||
|     return HTTPResponse(body, status=status, headers=headers, |     return HTTPResponse(body, status=status, headers=headers, | ||||||
|                         content_type="text/html; charset=utf-8") |                         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 | 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 | from .exceptions import NotFound, InvalidUsage | ||||||
|  |  | ||||||
| Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) | Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) | ||||||
| Parameter = namedtuple("Parameter", ['name', 'cast']) | 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: | class Router: | ||||||
| @@ -18,22 +35,16 @@ class Router: | |||||||
|     function provided Parameters can also have a type by appending :type to |     function provided Parameters can also have a type by appending :type to | ||||||
|     the <parameter>.  If no type is provided, a string is expected.  A regular |     the <parameter>.  If no type is provided, a string is expected.  A regular | ||||||
|     expression can also be passed in as the type |     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 |     routes_static = None | ||||||
|     regex_types = { |     routes_dynamic = None | ||||||
|         "string": (None, "[^/]+"), |     routes_always_check = None | ||||||
|         "int": (int, "\d+"), |  | ||||||
|         "number": (float, "[0-9\\.]+"), |  | ||||||
|         "alpha": (None, "[A-Za-z]+"), |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     def __init__(self): |     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): |     def add(self, uri, methods, handler): | ||||||
|         """ |         """ | ||||||
| @@ -45,42 +56,52 @@ class Router: | |||||||
|         When executed, it should provide a response object. |         When executed, it should provide a response object. | ||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|  |         if uri in self.routes_all: | ||||||
|  |             raise RouteExists("Route already registered: {}".format(uri)) | ||||||
|  |  | ||||||
|         # Dict for faster lookups of if method allowed |         # Dict for faster lookups of if method allowed | ||||||
|         methods_dict = None |  | ||||||
|         if methods: |         if methods: | ||||||
|             methods_dict = {method: True for method in methods} |             methods = frozenset(methods) | ||||||
|  |  | ||||||
|         parameters = [] |         parameters = [] | ||||||
|  |         properties = {"unhashable": None} | ||||||
|  |  | ||||||
|         def add_parameter(match): |         def add_parameter(match): | ||||||
|             # We could receive NAME or NAME:PATTERN |             # We could receive NAME or NAME:PATTERN | ||||||
|             parts = match.group(1).split(':') |             name = match.group(1) | ||||||
|             if len(parts) == 2: |             pattern = 'string' | ||||||
|                 parameter_name, parameter_pattern = parts |             if ':' in name: | ||||||
|             else: |                 name, pattern = name.split(':', 1) | ||||||
|                 parameter_name = parts[0] |  | ||||||
|                 parameter_pattern = 'string' |  | ||||||
|  |  | ||||||
|  |             default = (str, pattern) | ||||||
|             # Pull from pre-configured types |             # Pull from pre-configured types | ||||||
|             parameter_regex = self.regex_types.get(parameter_pattern) |             _type, pattern = REGEX_TYPES.get(pattern, default) | ||||||
|             if parameter_regex: |             parameter = Parameter(name=name, cast=_type) | ||||||
|                 parameter_type, parameter_pattern = parameter_regex |  | ||||||
|             else: |  | ||||||
|                 parameter_type = None |  | ||||||
|  |  | ||||||
|             parameter = Parameter(name=parameter_name, cast=parameter_type) |  | ||||||
|             parameters.append(parameter) |             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) |             return '({})'.format(pattern) | ||||||
|         pattern = re.compile("^{}$".format(pattern_string)) |  | ||||||
|  |         pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) | ||||||
|  |         pattern = re.compile(r'^{}$'.format(pattern_string)) | ||||||
|  |  | ||||||
|         route = Route( |         route = Route( | ||||||
|             handler=handler, methods=methods_dict, pattern=pattern, |             handler=handler, methods=methods, pattern=pattern, | ||||||
|             parameters=parameters) |             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): |     def get(self, request): | ||||||
|         """ |         """ | ||||||
| @@ -89,58 +110,42 @@ class Router: | |||||||
|         :param request: Request object |         :param request: Request object | ||||||
|         :return: handler, arguments, keyword arguments |         :return: handler, arguments, keyword arguments | ||||||
|         """ |         """ | ||||||
|  |         return self._get(request.url, request.method) | ||||||
|  |  | ||||||
|         route = None |     @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) | ||||||
|         args = [] |     def _get(self, url, method): | ||||||
|         kwargs = {} |         """ | ||||||
|         for _route in self.routes: |         Gets a request handler based on the URL of the request, or raises an | ||||||
|             match = _route.pattern.match(request.url) |         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: |                 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 |                     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: |             else: | ||||||
|             raise NotFound("Requested URL {} not found".format(request.url)) |                 # Lastly, check against all regex routes that cannot be hashed | ||||||
|  |                 for route in self.routes_always_check: | ||||||
|  |                     match = route.pattern.match(url) | ||||||
| class SimpleRouter: |                     if match: | ||||||
|     """ |                         break | ||||||
|     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, [], {} |  | ||||||
|                 else: |                 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 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 multiprocessing import Process, Event | ||||||
| from signal import signal, SIGTERM, SIGINT | from signal import signal, SIGTERM, SIGINT | ||||||
| from time import sleep | from time import sleep | ||||||
| @@ -11,19 +13,28 @@ from .log import log, logging | |||||||
| from .response import HTTPResponse | from .response import HTTPResponse | ||||||
| from .router import Router | from .router import Router | ||||||
| from .server import serve | from .server import serve | ||||||
|  | from .static import register as static_register | ||||||
| from .exceptions import ServerError | from .exceptions import ServerError | ||||||
|  |  | ||||||
|  |  | ||||||
| class Sanic: | 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.name = name | ||||||
|         self.router = router or Router() |         self.router = router or Router() | ||||||
|         self.error_handler = error_handler or Handler(self) |         self.error_handler = error_handler or Handler(self) | ||||||
|         self.config = Config() |         self.config = Config() | ||||||
|         self.request_middleware = [] |         self.request_middleware = deque() | ||||||
|         self.response_middleware = [] |         self.response_middleware = deque() | ||||||
|         self.blueprints = {} |         self.blueprints = {} | ||||||
|         self._blueprint_order = [] |         self._blueprint_order = [] | ||||||
|  |         self.loop = None | ||||||
|  |         self.debug = None | ||||||
|  |  | ||||||
|  |         # Register alternative method names | ||||||
|  |         self.go_fast = self.run | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # Registration |     # Registration | ||||||
| @@ -38,18 +49,35 @@ class Sanic: | |||||||
|         :return: decorated function |         :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): |         def response(handler): | ||||||
|             self.router.add(uri=uri, methods=methods, handler=handler) |             self.router.add(uri=uri, methods=methods, handler=handler) | ||||||
|             return handler |             return handler | ||||||
|  |  | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|  |     def add_route(self, handler, uri, methods=None): | ||||||
|  |         """ | ||||||
|  |         A helper method to register class instance or | ||||||
|  |         functions as a handler to the application url | ||||||
|  |         routes. | ||||||
|  |         :param handler: function or class instance | ||||||
|  |         :param uri: path of the URL | ||||||
|  |         :param methods: list or tuple of methods allowed | ||||||
|  |         :return: function or class instance | ||||||
|  |         """ | ||||||
|  |         self.route(uri=uri, methods=methods)(handler) | ||||||
|  |         return handler | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def exception(self, *exceptions): |     def exception(self, *exceptions): | ||||||
|         """ |         """ | ||||||
|         Decorates a function to be registered as a route |         Decorates a function to be registered as a handler for exceptions | ||||||
|         :param uri: path of the URL |         :param *exceptions: exceptions | ||||||
|         :param methods: list or tuple of methods allowed |  | ||||||
|         :return: decorated function |         :return: decorated function | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
| @@ -72,7 +100,7 @@ class Sanic: | |||||||
|             if attach_to == 'request': |             if attach_to == 'request': | ||||||
|                 self.request_middleware.append(middleware) |                 self.request_middleware.append(middleware) | ||||||
|             if attach_to == 'response': |             if attach_to == 'response': | ||||||
|                 self.response_middleware.append(middleware) |                 self.response_middleware.appendleft(middleware) | ||||||
|             return middleware |             return middleware | ||||||
|  |  | ||||||
|         # Detect which way this was called, @middleware or @middleware('AT') |         # Detect which way this was called, @middleware or @middleware('AT') | ||||||
| @@ -82,7 +110,17 @@ class Sanic: | |||||||
|             attach_to = args[0] |             attach_to = args[0] | ||||||
|             return register_middleware |             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. |         Registers a blueprint on the application. | ||||||
|         :param blueprint: Blueprint object |         :param blueprint: Blueprint object | ||||||
| @@ -99,10 +137,19 @@ class Sanic: | |||||||
|             self._blueprint_order.append(blueprint) |             self._blueprint_order.append(blueprint) | ||||||
|         blueprint.register(self, options) |         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 |     # Request Handling | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|  |  | ||||||
|  |     def converted_response_type(self, response): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|     async def handle_request(self, request, response_callback): |     async def handle_request(self, request, response_callback): | ||||||
|         """ |         """ | ||||||
|         Takes a request from the HTTP Server and returns a response object to |         Takes a request from the HTTP Server and returns a response object to | ||||||
| @@ -114,7 +161,10 @@ class Sanic: | |||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             # Middleware process_request |             # -------------------------------------------- # | ||||||
|  |             # Request Middleware | ||||||
|  |             # -------------------------------------------- # | ||||||
|  |  | ||||||
|             response = False |             response = False | ||||||
|             # The if improves speed.  I don't know why |             # The if improves speed.  I don't know why | ||||||
|             if self.request_middleware: |             if self.request_middleware: | ||||||
| @@ -127,6 +177,10 @@ class Sanic: | |||||||
|  |  | ||||||
|             # No middleware results |             # No middleware results | ||||||
|             if not response: |             if not response: | ||||||
|  |                 # -------------------------------------------- # | ||||||
|  |                 # Execute Handler | ||||||
|  |                 # -------------------------------------------- # | ||||||
|  |  | ||||||
|                 # Fetch handler from router |                 # Fetch handler from router | ||||||
|                 handler, args, kwargs = self.router.get(request) |                 handler, args, kwargs = self.router.get(request) | ||||||
|                 if handler is None: |                 if handler is None: | ||||||
| @@ -139,7 +193,10 @@ class Sanic: | |||||||
|                 if isawaitable(response): |                 if isawaitable(response): | ||||||
|                     response = await response |                     response = await response | ||||||
|  |  | ||||||
|                 # Middleware process_response |                 # -------------------------------------------- # | ||||||
|  |                 # Response Middleware | ||||||
|  |                 # -------------------------------------------- # | ||||||
|  |  | ||||||
|                 if self.response_middleware: |                 if self.response_middleware: | ||||||
|                     for middleware in self.response_middleware: |                     for middleware in self.response_middleware: | ||||||
|                         _response = middleware(request, response) |                         _response = middleware(request, response) | ||||||
| @@ -150,6 +207,10 @@ class Sanic: | |||||||
|                             break |                             break | ||||||
|  |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|  |             # -------------------------------------------- # | ||||||
|  |             # Response Generation Failed | ||||||
|  |             # -------------------------------------------- # | ||||||
|  |  | ||||||
|             try: |             try: | ||||||
|                 response = self.error_handler.response(request, e) |                 response = self.error_handler.response(request, e) | ||||||
|                 if isawaitable(response): |                 if isawaitable(response): | ||||||
| @@ -169,25 +230,32 @@ class Sanic: | |||||||
|     # Execution |     # Execution | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|  |  | ||||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, |     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, | ||||||
|             before_stop=None, sock=None, workers=1): |             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 |         Runs the HTTP Server and listens until keyboard interrupt or term | ||||||
|         signal. On termination, drains connections before closing. |         signal. On termination, drains connections before closing. | ||||||
|         :param host: Address to host on |         :param host: Address to host on | ||||||
|         :param port: Port to host on |         :param port: Port to host on | ||||||
|         :param debug: Enables debug output (slows server) |         :param debug: Enables debug output (slows server) | ||||||
|  |         :param before_start: Function to be executed before the server starts | ||||||
|  |         accepting connections | ||||||
|         :param after_start: Function to be executed after the server starts |         :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 |         :param before_stop: Function to be executed when a stop signal is | ||||||
|         received before it is respected |         received before it is respected | ||||||
|  |         :param after_stop: Function to be executed when all requests are | ||||||
|  |         complete | ||||||
|         :param sock: Socket for the server to accept connections from |         :param sock: Socket for the server to accept connections from | ||||||
|         :param workers: Number of processes |         :param workers: Number of processes | ||||||
|         received before it is respected |         received before it is respected | ||||||
|  |         :param loop: asyncio compatible event loop | ||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|         self.error_handler.debug = True |         self.error_handler.debug = True | ||||||
|         self.debug = debug |         self.debug = debug | ||||||
|  |         self.loop = loop | ||||||
|  |  | ||||||
|         server_settings = { |         server_settings = { | ||||||
|             'host': host, |             'host': host, | ||||||
| @@ -195,10 +263,35 @@ class Sanic: | |||||||
|             'sock': sock, |             'sock': sock, | ||||||
|             'debug': debug, |             'debug': debug, | ||||||
|             'request_handler': self.handle_request, |             'request_handler': self.handle_request, | ||||||
|  |             'error_handler': self.error_handler, | ||||||
|             'request_timeout': self.config.REQUEST_TIMEOUT, |             'request_timeout': self.config.REQUEST_TIMEOUT, | ||||||
|             'request_max_size': self.config.REQUEST_MAX_SIZE, |             'request_max_size': self.config.REQUEST_MAX_SIZE, | ||||||
|  |             'loop': loop | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         # -------------------------------------------- # | ||||||
|  |         # 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: |         if debug: | ||||||
|             log.setLevel(logging.DEBUG) |             log.setLevel(logging.DEBUG) | ||||||
|         log.debug(self.config.LOGO) |         log.debug(self.config.LOGO) | ||||||
| @@ -208,8 +301,6 @@ class Sanic: | |||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             if workers == 1: |             if workers == 1: | ||||||
|                 server_settings['after_start'] = after_start |  | ||||||
|                 server_settings['before_stop'] = before_stop |  | ||||||
|                 serve(**server_settings) |                 serve(**server_settings) | ||||||
|             else: |             else: | ||||||
|                 log.info('Spinning up {} workers...'.format(workers)) |                 log.info('Spinning up {} workers...'.format(workers)) | ||||||
| @@ -218,8 +309,7 @@ class Sanic: | |||||||
|  |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             log.exception( |             log.exception( | ||||||
|                 'Experienced exception while trying to serve: {}'.format(e)) |                 'Experienced exception while trying to serve') | ||||||
|             pass |  | ||||||
|  |  | ||||||
|         log.info("Server Stopped") |         log.info("Server Stopped") | ||||||
|  |  | ||||||
| @@ -248,7 +338,7 @@ class Sanic: | |||||||
|         signal(SIGTERM, lambda s, f: stop_event.set()) |         signal(SIGTERM, lambda s, f: stop_event.set()) | ||||||
|  |  | ||||||
|         processes = [] |         processes = [] | ||||||
|         for w in range(workers): |         for _ in range(workers): | ||||||
|             process = Process(target=serve, kwargs=server_settings) |             process = Process(target=serve, kwargs=server_settings) | ||||||
|             process.start() |             process.start() | ||||||
|             processes.append(process) |             processes.append(process) | ||||||
|   | |||||||
							
								
								
									
										140
									
								
								sanic/server.py
									
									
									
									
									
								
							
							
						
						
									
										140
									
								
								sanic/server.py
									
									
									
									
									
								
							| @@ -1,8 +1,11 @@ | |||||||
| import asyncio | import asyncio | ||||||
|  | from functools import partial | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
|  | from multidict import CIMultiDict | ||||||
| from signal import SIGINT, SIGTERM | from signal import SIGINT, SIGTERM | ||||||
|  | from time import time | ||||||
| import httptools | from httptools import HttpRequestParser | ||||||
|  | from httptools.parser.errors import HttpParserError | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     import uvloop as async_loop |     import uvloop as async_loop | ||||||
| @@ -11,12 +14,16 @@ except ImportError: | |||||||
|  |  | ||||||
| from .log import log | from .log import log | ||||||
| from .request import Request | from .request import Request | ||||||
|  | from .exceptions import RequestTimeout | ||||||
|  |  | ||||||
|  |  | ||||||
| class Signal: | class Signal: | ||||||
|     stopped = False |     stopped = False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | current_time = None | ||||||
|  |  | ||||||
|  |  | ||||||
| class HttpProtocol(asyncio.Protocol): | class HttpProtocol(asyncio.Protocol): | ||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         # event loop, connection |         # event loop, connection | ||||||
| @@ -26,10 +33,10 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         # request config |         # request config | ||||||
|         'request_handler', 'request_timeout', 'request_max_size', |         'request_handler', 'request_timeout', 'request_max_size', | ||||||
|         # connection management |         # connection management | ||||||
|         '_total_request_size', '_timeout_handler') |         '_total_request_size', '_timeout_handler', '_last_communication_time') | ||||||
|  |  | ||||||
|     def __init__(self, *, loop, request_handler, signal=Signal(), |     def __init__(self, *, loop, request_handler, error_handler, | ||||||
|                  connections={}, request_timeout=60, |                  signal=Signal(), connections={}, request_timeout=60, | ||||||
|                  request_max_size=None): |                  request_max_size=None): | ||||||
|         self.loop = loop |         self.loop = loop | ||||||
|         self.transport = None |         self.transport = None | ||||||
| @@ -40,13 +47,15 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.signal = signal |         self.signal = signal | ||||||
|         self.connections = connections |         self.connections = connections | ||||||
|         self.request_handler = request_handler |         self.request_handler = request_handler | ||||||
|  |         self.error_handler = error_handler | ||||||
|         self.request_timeout = request_timeout |         self.request_timeout = request_timeout | ||||||
|         self.request_max_size = request_max_size |         self.request_max_size = request_max_size | ||||||
|         self._total_request_size = 0 |         self._total_request_size = 0 | ||||||
|         self._timeout_handler = None |         self._timeout_handler = None | ||||||
|  |         self._last_request_time = None | ||||||
|  |         self._request_handler_task = None | ||||||
|  |  | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|  |  | ||||||
|     # Connection |     # Connection | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|  |  | ||||||
| @@ -55,6 +64,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self._timeout_handler = self.loop.call_later( |         self._timeout_handler = self.loop.call_later( | ||||||
|             self.request_timeout, self.connection_timeout) |             self.request_timeout, self.connection_timeout) | ||||||
|         self.transport = transport |         self.transport = transport | ||||||
|  |         self._last_request_time = current_time | ||||||
|  |  | ||||||
|     def connection_lost(self, exc): |     def connection_lost(self, exc): | ||||||
|         del self.connections[self] |         del self.connections[self] | ||||||
| @@ -62,10 +72,20 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.cleanup() |         self.cleanup() | ||||||
|  |  | ||||||
|     def connection_timeout(self): |     def connection_timeout(self): | ||||||
|         self.bail_out("Request timed out, connection closed") |         # Check if | ||||||
|  |         time_elapsed = current_time - self._last_request_time | ||||||
|  |         if time_elapsed < self.request_timeout: | ||||||
|  |             time_left = self.request_timeout - time_elapsed | ||||||
|  |             self._timeout_handler = \ | ||||||
|  |                 self.loop.call_later(time_left, self.connection_timeout) | ||||||
|  |         else: | ||||||
|  |             if self._request_handler_task: | ||||||
|  |                 self._request_handler_task.cancel() | ||||||
|  |             response = self.error_handler.response( | ||||||
|  |                 self.request, RequestTimeout('Request Timeout')) | ||||||
|  |             self.write_response(response) | ||||||
|  |  | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|  |  | ||||||
|     # Parsing |     # Parsing | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|  |  | ||||||
| @@ -82,12 +102,12 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         if self.parser is None: |         if self.parser is None: | ||||||
|             assert self.request is None |             assert self.request is None | ||||||
|             self.headers = [] |             self.headers = [] | ||||||
|             self.parser = httptools.HttpRequestParser(self) |             self.parser = HttpRequestParser(self) | ||||||
|  |  | ||||||
|         # Parse request chunk or close connection |         # Parse request chunk or close connection | ||||||
|         try: |         try: | ||||||
|             self.parser.feed_data(data) |             self.parser.feed_data(data) | ||||||
|         except httptools.parser.errors.HttpParserError as e: |         except HttpParserError as e: | ||||||
|             self.bail_out( |             self.bail_out( | ||||||
|                 "Invalid request data, connection closed ({})".format(e)) |                 "Invalid request data, connection closed ({})".format(e)) | ||||||
|  |  | ||||||
| @@ -102,18 +122,25 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.headers.append((name.decode(), value.decode('utf-8'))) |         self.headers.append((name.decode(), value.decode('utf-8'))) | ||||||
|  |  | ||||||
|     def on_headers_complete(self): |     def on_headers_complete(self): | ||||||
|  |         remote_addr = self.transport.get_extra_info('peername') | ||||||
|  |         if remote_addr: | ||||||
|  |             self.headers.append(('Remote-Addr', '%s:%s' % remote_addr)) | ||||||
|  |  | ||||||
|         self.request = Request( |         self.request = Request( | ||||||
|             url_bytes=self.url, |             url_bytes=self.url, | ||||||
|             headers=dict(self.headers), |             headers=CIMultiDict(self.headers), | ||||||
|             version=self.parser.get_http_version(), |             version=self.parser.get_http_version(), | ||||||
|             method=self.parser.get_method().decode() |             method=self.parser.get_method().decode() | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     def on_body(self, body): |     def on_body(self, body): | ||||||
|  |         if self.request.body: | ||||||
|  |             self.request.body += body | ||||||
|  |         else: | ||||||
|             self.request.body = body |             self.request.body = body | ||||||
|  |  | ||||||
|     def on_message_complete(self): |     def on_message_complete(self): | ||||||
|         self.loop.create_task( |         self._request_handler_task = self.loop.create_task( | ||||||
|             self.request_handler(self.request, self.write_response)) |             self.request_handler(self.request, self.write_response)) | ||||||
|  |  | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
| @@ -130,13 +157,15 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             if not keep_alive: |             if not keep_alive: | ||||||
|                 self.transport.close() |                 self.transport.close() | ||||||
|             else: |             else: | ||||||
|  |                 # Record that we received data | ||||||
|  |                 self._last_request_time = current_time | ||||||
|                 self.cleanup() |                 self.cleanup() | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             self.bail_out( |             self.bail_out( | ||||||
|                 "Writing request failed, connection closed {}".format(e)) |                 "Writing response failed, connection closed {}".format(e)) | ||||||
|  |  | ||||||
|     def bail_out(self, message): |     def bail_out(self, message): | ||||||
|         log.error(message) |         log.debug(message) | ||||||
|         self.transport.close() |         self.transport.close() | ||||||
|  |  | ||||||
|     def cleanup(self): |     def cleanup(self): | ||||||
| @@ -144,6 +173,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.request = None |         self.request = None | ||||||
|         self.url = None |         self.url = None | ||||||
|         self.headers = None |         self.headers = None | ||||||
|  |         self._request_handler_task = None | ||||||
|         self._total_request_size = 0 |         self._total_request_size = 0 | ||||||
|  |  | ||||||
|     def close_if_idle(self): |     def close_if_idle(self): | ||||||
| @@ -157,15 +187,60 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         return False |         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, |           debug=False, request_timeout=60, sock=None, | ||||||
|           request_max_size=None, reuse_port=False): |           request_max_size=None, reuse_port=False, loop=None): | ||||||
|     # Create Event Loop |     """ | ||||||
|     loop = async_loop.new_event_loop() |     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) |     asyncio.set_event_loop(loop) | ||||||
|     # I don't think we take advantage of this |  | ||||||
|     # And it slows everything waaayyy down |     if debug: | ||||||
|     # loop.set_debug(debug) |         loop.set_debug(debug) | ||||||
|  |  | ||||||
|  |     trigger_events(before_start, loop) | ||||||
|  |  | ||||||
|     connections = {} |     connections = {} | ||||||
|     signal = Signal() |     signal = Signal() | ||||||
| @@ -174,20 +249,22 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | |||||||
|         connections=connections, |         connections=connections, | ||||||
|         signal=signal, |         signal=signal, | ||||||
|         request_handler=request_handler, |         request_handler=request_handler, | ||||||
|  |         error_handler=error_handler, | ||||||
|         request_timeout=request_timeout, |         request_timeout=request_timeout, | ||||||
|         request_max_size=request_max_size, |         request_max_size=request_max_size, | ||||||
|     ), host, port, reuse_port=reuse_port, sock=sock) |     ), 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: |     try: | ||||||
|         http_server = loop.run_until_complete(server_coroutine) |         http_server = loop.run_until_complete(server_coroutine) | ||||||
|     except Exception as e: |     except Exception: | ||||||
|         log.error("Unable to start server: {}".format(e)) |         log.exception("Unable to start server") | ||||||
|         return |         return | ||||||
|  |  | ||||||
|     # Run the on_start function if provided |     trigger_events(after_start, loop) | ||||||
|     if after_start: |  | ||||||
|         result = after_start(loop) |  | ||||||
|         if isawaitable(result): |  | ||||||
|             loop.run_until_complete(result) |  | ||||||
|  |  | ||||||
|     # Register signals for graceful termination |     # Register signals for graceful termination | ||||||
|     for _signal in (SIGINT, SIGTERM): |     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...") |         log.info("Stop requested, draining connections...") | ||||||
|  |  | ||||||
|         # Run the on_stop function if provided |         # Run the on_stop function if provided | ||||||
|         if before_stop: |         trigger_events(before_stop, loop) | ||||||
|             result = before_stop(loop) |  | ||||||
|             if isawaitable(result): |  | ||||||
|                 loop.run_until_complete(result) |  | ||||||
|  |  | ||||||
|         # Wait for event loop to finish and all connections to drain |         # Wait for event loop to finish and all connections to drain | ||||||
|         http_server.close() |         http_server.close() | ||||||
| @@ -216,4 +290,6 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | |||||||
|         while connections: |         while connections: | ||||||
|             loop.run_until_complete(asyncio.sleep(0.1)) |             loop.run_until_complete(asyncio.sleep(0.1)) | ||||||
|  |  | ||||||
|  |         trigger_events(after_stop, loop) | ||||||
|  |  | ||||||
|         loop.close() |         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 | 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) |     url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) | ||||||
|     log.info(url) |     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: |         async with getattr(session, method)(url, *args, **kwargs) as response: | ||||||
|             response.text = await response.text() |             response.text = await response.text() | ||||||
|  |             response.body = await response.read() | ||||||
|             return response |             return response | ||||||
|  |  | ||||||
|  |  | ||||||
| def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||||
|                         *request_args, **request_kwargs): |                         loop=None, debug=False, *request_args, | ||||||
|  |                         **request_kwargs): | ||||||
|     results = [] |     results = [] | ||||||
|     exceptions = [] |     exceptions = [] | ||||||
|  |  | ||||||
| @@ -24,7 +26,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | |||||||
|         def _collect_request(request): |         def _collect_request(request): | ||||||
|             results.append(request) |             results.append(request) | ||||||
|  |  | ||||||
|     async def _collect_response(loop): |     async def _collect_response(sanic, loop): | ||||||
|         try: |         try: | ||||||
|             response = await local_request(method, uri, *request_args, |             response = await local_request(method, uri, *request_args, | ||||||
|                                            **request_kwargs) |                                            **request_kwargs) | ||||||
| @@ -33,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | |||||||
|             exceptions.append(e) |             exceptions.append(e) | ||||||
|         app.stop() |         app.stop() | ||||||
|  |  | ||||||
|     app.run(host=HOST, port=42101, after_start=_collect_response) |     app.run(host=HOST, debug=debug, port=42101, | ||||||
|  |             after_start=_collect_response, loop=loop) | ||||||
|  |  | ||||||
|     if exceptions: |     if exceptions: | ||||||
|         raise ValueError("Exception during request: {}".format(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 | Sanic | ||||||
| """ | """ | ||||||
|  | import codecs | ||||||
|  | import os | ||||||
|  | import re | ||||||
| from setuptools import setup | 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( | setup( | ||||||
|     name='Sanic', |     name='Sanic', | ||||||
|     version="0.1.4", |     version=version, | ||||||
|     url='http://github.com/channelcat/sanic/', |     url='http://github.com/channelcat/sanic/', | ||||||
|     license='MIT', |     license='MIT', | ||||||
|     author='Channel Cat', |     author='Channel Cat', | ||||||
| @@ -17,6 +29,8 @@ setup( | |||||||
|         'uvloop>=0.5.3', |         'uvloop>=0.5.3', | ||||||
|         'httptools>=0.0.9', |         'httptools>=0.0.9', | ||||||
|         'ujson>=1.35', |         'ujson>=1.35', | ||||||
|  |         'aiofiles>=0.3.0', | ||||||
|  |         'multidict>=2.0', | ||||||
|     ], |     ], | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|         'Development Status :: 2 - Pre-Alpha', |         '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 = web.Application(loop=loop) | ||||||
| app.router.add_route('GET', '/', handle) | app.router.add_route('GET', '/', handle) | ||||||
|  |  | ||||||
| web.run_app(app, port=sys.argv[1]) | web.run_app(app, port=sys.argv[1], access_log=None) | ||||||
|   | |||||||
							
								
								
									
										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 import Sanic | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
| from sanic.response import json, text | from sanic.response import json, text | ||||||
| @@ -17,7 +19,7 @@ def test_bp(): | |||||||
|     def handler(request): |     def handler(request): | ||||||
|         return text('Hello') |         return text('Hello') | ||||||
|  |  | ||||||
|     app.register_blueprint(bp) |     app.blueprint(bp) | ||||||
|     request, response = sanic_endpoint_test(app) |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
|     assert response.text == 'Hello' |     assert response.text == 'Hello' | ||||||
| @@ -30,7 +32,7 @@ def test_bp_with_url_prefix(): | |||||||
|     def handler(request): |     def handler(request): | ||||||
|         return text('Hello') |         return text('Hello') | ||||||
|  |  | ||||||
|     app.register_blueprint(bp) |     app.blueprint(bp) | ||||||
|     request, response = sanic_endpoint_test(app, uri='/test1/') |     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||||
|  |  | ||||||
|     assert response.text == 'Hello' |     assert response.text == 'Hello' | ||||||
| @@ -49,8 +51,8 @@ def test_several_bp_with_url_prefix(): | |||||||
|     def handler2(request): |     def handler2(request): | ||||||
|         return text('Hello2') |         return text('Hello2') | ||||||
|  |  | ||||||
|     app.register_blueprint(bp) |     app.blueprint(bp) | ||||||
|     app.register_blueprint(bp2) |     app.blueprint(bp2) | ||||||
|     request, response = sanic_endpoint_test(app, uri='/test1/') |     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||||
|     assert response.text == 'Hello' |     assert response.text == 'Hello' | ||||||
|  |  | ||||||
| @@ -70,7 +72,7 @@ def test_bp_middleware(): | |||||||
|     async def handler(request): |     async def handler(request): | ||||||
|         return text('FAIL') |         return text('FAIL') | ||||||
|  |  | ||||||
|     app.register_blueprint(blueprint) |     app.blueprint(blueprint) | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app) |     request, response = sanic_endpoint_test(app) | ||||||
|  |  | ||||||
| @@ -97,7 +99,7 @@ def test_bp_exception_handler(): | |||||||
|     def handler_exception(request, exception): |     def handler_exception(request, exception): | ||||||
|         return text("OK") |         return text("OK") | ||||||
|  |  | ||||||
|     app.register_blueprint(blueprint) |     app.blueprint(blueprint) | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, uri='/1') |     request, response = sanic_endpoint_test(app, uri='/1') | ||||||
|     assert response.status == 400 |     assert response.status == 400 | ||||||
| @@ -109,3 +111,55 @@ def test_bp_exception_handler(): | |||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, uri='/3') |     request, response = sanic_endpoint_test(app, uri='/3') | ||||||
|     assert response.status == 200 |     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.status == 200 | ||||||
|     assert response.text == 'OK' |     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): |     async def handler(request): | ||||||
|         return text('OK') |         return text('OK') | ||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")]) |     request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")]) | ||||||
|  |  | ||||||
|     assert request.args.get('test1') == '1' |     assert request.args.get('test1') == '1' | ||||||
|     assert request.args.get('test2') == 'false' |     assert request.args.get('test2') == 'false' | ||||||
| @@ -80,3 +80,38 @@ def test_post_json(): | |||||||
|  |  | ||||||
|     assert request.json.get('test') == 'OK' |     assert request.json.get('test') == 'OK' | ||||||
|     assert response.text == '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 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 | from sanic.utils import sanic_endpoint_test | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -8,6 +10,24 @@ from sanic.utils import sanic_endpoint_test | |||||||
| #  UTF-8 | #  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(): | def test_dynamic_route(): | ||||||
|     app = Sanic('test_dynamic_route') |     app = Sanic('test_dynamic_route') | ||||||
|  |  | ||||||
| @@ -64,7 +84,7 @@ def test_dynamic_route_int(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_dynamic_route_number(): | def test_dynamic_route_number(): | ||||||
|     app = Sanic('test_dynamic_route_int') |     app = Sanic('test_dynamic_route_number') | ||||||
|  |  | ||||||
|     results = [] |     results = [] | ||||||
|  |  | ||||||
| @@ -85,7 +105,7 @@ def test_dynamic_route_number(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_dynamic_route_regex(): | def test_dynamic_route_regex(): | ||||||
|     app = Sanic('test_dynamic_route_int') |     app = Sanic('test_dynamic_route_regex') | ||||||
|  |  | ||||||
|     @app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>') |     @app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>') | ||||||
|     async def handler(request, folder_id): |     async def handler(request, folder_id): | ||||||
| @@ -102,3 +122,237 @@ def test_dynamic_route_regex(): | |||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, uri='/folder/') |     request, response = sanic_endpoint_test(app, uri='/folder/') | ||||||
|     assert response.status == 200 |     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