Compare commits
	
		
			78 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 | ||
|   | f95fe4192b | 
| @@ -1,5 +1,7 @@ | |||||||
| # Sanic | # Sanic | ||||||
|  |  | ||||||
|  | [](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||||||
|  |  | ||||||
| [](https://travis-ci.org/channelcat/sanic) | [](https://travis-ci.org/channelcat/sanic) | ||||||
| [](https://pypi.python.org/pypi/sanic/) | [](https://pypi.python.org/pypi/sanic/) | ||||||
| [](https://pypi.python.org/pypi/sanic/) | [](https://pypi.python.org/pypi/sanic/) | ||||||
| @@ -31,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) | ||||||
| ``` | ``` | ||||||
| @@ -50,6 +52,7 @@ app.run(host="0.0.0.0", port=8000) | |||||||
|  * [Middleware](docs/middleware.md) |  * [Middleware](docs/middleware.md) | ||||||
|  * [Exceptions](docs/exceptions.md) |  * [Exceptions](docs/exceptions.md) | ||||||
|  * [Blueprints](docs/blueprints.md) |  * [Blueprints](docs/blueprints.md) | ||||||
|  |  * [Class Based Views](docs/class_based_views.md) | ||||||
|  * [Cookies](docs/cookies.md) |  * [Cookies](docs/cookies.md) | ||||||
|  * [Static Files](docs/static_files.md) |  * [Static Files](docs/static_files.md) | ||||||
|  * [Deploying](docs/deploying.md) |  * [Deploying](docs/deploying.md) | ||||||
| @@ -70,7 +73,7 @@ app.run(host="0.0.0.0", port=8000) | |||||||
|                      ▄▄▄▄▄ |                      ▄▄▄▄▄ | ||||||
|             ▀▀▀██████▄▄▄       _______________ |             ▀▀▀██████▄▄▄       _______________ | ||||||
|           ▄▄▄▄▄  █████████▄  /                 \ |           ▄▄▄▄▄  █████████▄  /                 \ | ||||||
|          ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  |  |          ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  | | ||||||
|        ▀▀█████▄▄ ▀██████▄██ | _________________/ |        ▀▀█████▄▄ ▀██████▄██ | _________________/ | ||||||
|        ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/ |        ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/ | ||||||
|             ▀▀▀▄  ▀▀███ ▀       ▄▄ |             ▀▀▀▄  ▀▀███ ▀       ▄▄ | ||||||
|   | |||||||
							
								
								
									
										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') | ||||||
|  |  | ||||||
|  | ``` | ||||||
| @@ -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]>') | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
|   | |||||||
							
								
								
									
										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) | ||||||
| @@ -2,6 +2,7 @@ httptools | |||||||
| ujson | ujson | ||||||
| uvloop | uvloop | ||||||
| aiohttp | aiohttp | ||||||
|  | aiocache | ||||||
| pytest | pytest | ||||||
| coverage | coverage | ||||||
| tox | tox | ||||||
| @@ -9,4 +10,5 @@ gunicorn | |||||||
| bottle | bottle | ||||||
| kyoukai | kyoukai | ||||||
| falcon | falcon | ||||||
| tornado | tornado | ||||||
|  | aiofiles | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
| httptools | httptools | ||||||
| ujson | ujson | ||||||
| uvloop | uvloop | ||||||
|  | aiofiles | ||||||
|  | multidict | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from .sanic import Sanic | from .sanic import Sanic | ||||||
| from .blueprints import Blueprint | from .blueprints import Blueprint | ||||||
|  |  | ||||||
| __version__ = '0.1.7' | __version__ = '0.1.8' | ||||||
|  |  | ||||||
| __all__ = ['Sanic', 'Blueprint'] | __all__ = ['Sanic', 'Blueprint'] | ||||||
|   | |||||||
| @@ -91,6 +91,12 @@ class Blueprint: | |||||||
|             return handler |             return handler | ||||||
|         return decorator |         return decorator | ||||||
|  |  | ||||||
|  |     def add_route(self, handler, uri, methods=None): | ||||||
|  |         """ | ||||||
|  |         """ | ||||||
|  |         self.record(lambda s: s.add_route(handler, uri, methods)) | ||||||
|  |         return handler | ||||||
|  |  | ||||||
|     def listener(self, event): |     def listener(self, event): | ||||||
|         """ |         """ | ||||||
|         """ |         """ | ||||||
| @@ -109,8 +115,9 @@ class Blueprint: | |||||||
|  |  | ||||||
|         # Detect which way this was called, @middleware or @middleware('AT') |         # Detect which way this was called, @middleware or @middleware('AT') | ||||||
|         if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): |         if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): | ||||||
|  |             middleware = args[0] | ||||||
|             args = [] |             args = [] | ||||||
|             return register_middleware(args[0]) |             return register_middleware(middleware) | ||||||
|         else: |         else: | ||||||
|             return register_middleware |             return register_middleware | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ def _quote(str): | |||||||
|     else: |     else: | ||||||
|         return '"' + str.translate(_Translator) + '"' |         return '"' + str.translate(_Translator) + '"' | ||||||
|  |  | ||||||
|  |  | ||||||
| _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch | _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch | ||||||
|  |  | ||||||
| # ------------------------------------------------------------ # | # ------------------------------------------------------------ # | ||||||
|   | |||||||
| @@ -30,6 +30,10 @@ class FileNotFound(NotFound): | |||||||
|         self.relative_url = relative_url |         self.relative_url = relative_url | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestTimeout(SanicException): | ||||||
|  |     status_code = 408 | ||||||
|  |  | ||||||
|  |  | ||||||
| class Handler: | class Handler: | ||||||
|     handlers = None |     handlers = None | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,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 | ||||||
| @@ -26,7 +32,7 @@ class RequestParameters(dict): | |||||||
|         return self.super.get(name, default) |         return self.super.get(name, default) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Request: | class Request(dict): | ||||||
|     """ |     """ | ||||||
|     Properties of an HTTP request such as URL, headers, etc. |     Properties of an HTTP request such as URL, headers, etc. | ||||||
|     """ |     """ | ||||||
| @@ -61,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': | ||||||
| @@ -83,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 | ||||||
|  |  | ||||||
| @@ -128,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]: | ||||||
| @@ -162,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: | ||||||
|  |                 files[field_name] = [file] | ||||||
|         else: |         else: | ||||||
|             fields[field_name] = post_data.decode('utf-8') |             value = post_data.decode('utf-8') | ||||||
|  |             if field_name in fields: | ||||||
|  |                 fields[field_name].append(value) | ||||||
|  |             else: | ||||||
|  |                 fields[field_name] = [value] | ||||||
|  |  | ||||||
|     return fields, files |     return fields, files | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| from asyncio import get_event_loop | from asyncio import get_event_loop | ||||||
| from collections import deque | from collections import deque | ||||||
| from functools import partial | from functools import partial | ||||||
| from inspect import isawaitable | from inspect import isawaitable, stack, getmodulename | ||||||
| from multiprocessing import Process, Event | from multiprocessing import Process, Event | ||||||
| from signal import signal, SIGTERM, SIGINT | from signal import signal, SIGTERM, SIGINT | ||||||
| from time import sleep | from time import sleep | ||||||
| @@ -18,7 +18,10 @@ 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) | ||||||
| @@ -57,6 +60,19 @@ class Sanic: | |||||||
|  |  | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|  |     def add_route(self, handler, uri, methods=None): | ||||||
|  |         """ | ||||||
|  |         A helper method to register class instance or | ||||||
|  |         functions as a handler to the application url | ||||||
|  |         routes. | ||||||
|  |         :param handler: function or class instance | ||||||
|  |         :param uri: path of the URL | ||||||
|  |         :param methods: list or tuple of methods allowed | ||||||
|  |         :return: function or class instance | ||||||
|  |         """ | ||||||
|  |         self.route(uri=uri, methods=methods)(handler) | ||||||
|  |         return handler | ||||||
|  |  | ||||||
|     # Decorator |     # Decorator | ||||||
|     def exception(self, *exceptions): |     def exception(self, *exceptions): | ||||||
|         """ |         """ | ||||||
| @@ -247,6 +263,7 @@ 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 |             'loop': loop | ||||||
| @@ -292,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") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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,9 +122,13 @@ 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() | ||||||
|         ) |         ) | ||||||
| @@ -116,7 +140,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             self.request.body = body |             self.request.body = body | ||||||
|  |  | ||||||
|     def on_message_complete(self): |     def on_message_complete(self): | ||||||
|         self.loop.create_task( |         self._request_handler_task = self.loop.create_task( | ||||||
|             self.request_handler(self.request, self.write_response)) |             self.request_handler(self.request, self.write_response)) | ||||||
|  |  | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
| @@ -133,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): | ||||||
| @@ -147,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): | ||||||
| @@ -160,6 +187,18 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         return False |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def update_current_time(loop): | ||||||
|  |     """ | ||||||
|  |     Caches the current time, since it is needed | ||||||
|  |     at the end of every keep-alive request to update the request timeout time | ||||||
|  |     :param loop: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     global current_time | ||||||
|  |     current_time = time() | ||||||
|  |     loop.call_later(1, partial(update_current_time, loop)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def trigger_events(events, loop): | def trigger_events(events, loop): | ||||||
|     """ |     """ | ||||||
|     :param events: one or more sync or async functions to execute |     :param events: one or more sync or async functions to execute | ||||||
| @@ -174,8 +213,8 @@ def trigger_events(events, loop): | |||||||
|                 loop.run_until_complete(result) |                 loop.run_until_complete(result) | ||||||
|  |  | ||||||
|  |  | ||||||
| def serve(host, port, request_handler, before_start=None, after_start=None, | def serve(host, port, request_handler, error_handler, before_start=None, | ||||||
|           before_stop=None, after_stop=None, |           after_start=None, before_stop=None, after_stop=None, | ||||||
|           debug=False, request_timeout=60, sock=None, |           debug=False, request_timeout=60, sock=None, | ||||||
|           request_max_size=None, reuse_port=False, loop=None): |           request_max_size=None, reuse_port=False, loop=None): | ||||||
|     """ |     """ | ||||||
| @@ -210,10 +249,15 @@ def serve(host, port, request_handler, before_start=None, after_start=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: |     except Exception: | ||||||
|   | |||||||
| @@ -16,7 +16,8 @@ async def local_request(method, uri, cookies=None, *args, **kwargs): | |||||||
|  |  | ||||||
|  |  | ||||||
| def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | 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 = [] | ||||||
|  |  | ||||||
| @@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | |||||||
|             exceptions.append(e) |             exceptions.append(e) | ||||||
|         app.stop() |         app.stop() | ||||||
|  |  | ||||||
|     app.run(host=HOST, port=42101, after_start=_collect_response) |     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) | ||||||
							
								
								
									
										1
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								setup.py
									
									
									
									
									
								
							| @@ -30,6 +30,7 @@ setup( | |||||||
|         'httptools>=0.0.9', |         'httptools>=0.0.9', | ||||||
|         'ujson>=1.35', |         'ujson>=1.35', | ||||||
|         'aiofiles>=0.3.0', |         'aiofiles>=0.3.0', | ||||||
|  |         'multidict>=2.0', | ||||||
|     ], |     ], | ||||||
|     classifiers=[ |     classifiers=[ | ||||||
|         'Development Status :: 2 - Pre-Alpha', |         'Development Status :: 2 - Pre-Alpha', | ||||||
|   | |||||||
| @@ -15,4 +15,4 @@ async def handle(request): | |||||||
| app = web.Application(loop=loop) | app = web.Application(loop=loop) | ||||||
| app.router.add_route('GET', '/', handle) | app.router.add_route('GET', '/', handle) | ||||||
|  |  | ||||||
| web.run_app(app, port=sys.argv[1]) | web.run_app(app, port=sys.argv[1], access_log=None) | ||||||
|   | |||||||
							
								
								
									
										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' | ||||||
|   | |||||||
| @@ -84,7 +84,7 @@ def test_dynamic_route_int(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_dynamic_route_number(): | def test_dynamic_route_number(): | ||||||
|     app = Sanic('test_dynamic_route_int') |     app = Sanic('test_dynamic_route_number') | ||||||
|  |  | ||||||
|     results = [] |     results = [] | ||||||
|  |  | ||||||
| @@ -105,7 +105,7 @@ def test_dynamic_route_number(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_dynamic_route_regex(): | def test_dynamic_route_regex(): | ||||||
|     app = Sanic('test_dynamic_route_int') |     app = Sanic('test_dynamic_route_regex') | ||||||
|  |  | ||||||
|     @app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>') |     @app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>') | ||||||
|     async def handler(request, folder_id): |     async def handler(request, folder_id): | ||||||
| @@ -145,7 +145,7 @@ def test_dynamic_route_unhashable(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def test_route_duplicate(): | def test_route_duplicate(): | ||||||
|     app = Sanic('test_dynamic_route') |     app = Sanic('test_route_duplicate') | ||||||
|  |  | ||||||
|     with pytest.raises(RouteExists): |     with pytest.raises(RouteExists): | ||||||
|         @app.route('/test') |         @app.route('/test') | ||||||
| @@ -178,3 +178,181 @@ def test_method_not_allowed(): | |||||||
|  |  | ||||||
|     request, response = sanic_endpoint_test(app, method='post', uri='/test') |     request, response = sanic_endpoint_test(app, method='post', uri='/test') | ||||||
|     assert response.status == 405 |     assert response.status == 405 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_static_add_route(): | ||||||
|  |     app = Sanic('test_static_add_route') | ||||||
|  |  | ||||||
|  |     async def handler1(request): | ||||||
|  |         return text('OK1') | ||||||
|  |  | ||||||
|  |     async def handler2(request): | ||||||
|  |         return text('OK2') | ||||||
|  |  | ||||||
|  |     app.add_route(handler1, '/test') | ||||||
|  |     app.add_route(handler2, '/test2') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.text == 'OK1' | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test2') | ||||||
|  |     assert response.text == 'OK2' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route(): | ||||||
|  |     app = Sanic('test_dynamic_add_route') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     async def handler(request, name): | ||||||
|  |         results.append(name) | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<name>') | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test123') | ||||||
|  |  | ||||||
|  |     assert response.text == 'OK' | ||||||
|  |     assert results[0] == 'test123' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_string(): | ||||||
|  |     app = Sanic('test_dynamic_add_route_string') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     async def handler(request, name): | ||||||
|  |         results.append(name) | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<name:string>') | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test123') | ||||||
|  |  | ||||||
|  |     assert response.text == 'OK' | ||||||
|  |     assert results[0] == 'test123' | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico') | ||||||
|  |  | ||||||
|  |     assert response.text == 'OK' | ||||||
|  |     assert results[1] == 'favicon.ico' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_int(): | ||||||
|  |     app = Sanic('test_dynamic_add_route_int') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     async def handler(request, folder_id): | ||||||
|  |         results.append(folder_id) | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<folder_id:int>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/12345') | ||||||
|  |     assert response.text == 'OK' | ||||||
|  |     assert type(results[0]) is int | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/asdf') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_number(): | ||||||
|  |     app = Sanic('test_dynamic_add_route_number') | ||||||
|  |  | ||||||
|  |     results = [] | ||||||
|  |  | ||||||
|  |     async def handler(request, weight): | ||||||
|  |         results.append(weight) | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/weight/<weight:number>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/weight/12345') | ||||||
|  |     assert response.text == 'OK' | ||||||
|  |     assert type(results[0]) is float | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/weight/1234.56') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/weight/1234-56') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_regex(): | ||||||
|  |     app = Sanic('test_dynamic_route_int') | ||||||
|  |  | ||||||
|  |     async def handler(request, folder_id): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test1') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test-123') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_dynamic_add_route_unhashable(): | ||||||
|  |     app = Sanic('test_dynamic_add_route_unhashable') | ||||||
|  |  | ||||||
|  |     async def handler(request, unhashable): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/') | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/end/') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') | ||||||
|  |     assert response.status == 404 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_add_route_duplicate(): | ||||||
|  |     app = Sanic('test_add_route_duplicate') | ||||||
|  |  | ||||||
|  |     with pytest.raises(RouteExists): | ||||||
|  |         async def handler1(request): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         async def handler2(request): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         app.add_route(handler1, '/test') | ||||||
|  |         app.add_route(handler2, '/test') | ||||||
|  |  | ||||||
|  |     with pytest.raises(RouteExists): | ||||||
|  |         async def handler1(request, dynamic): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         async def handler2(request, dynamic): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         app.add_route(handler1, '/test/<dynamic>/') | ||||||
|  |         app.add_route(handler2, '/test/<dynamic>/') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_add_route_method_not_allowed(): | ||||||
|  |     app = Sanic('test_add_route_method_not_allowed') | ||||||
|  |  | ||||||
|  |     async def handler(request): | ||||||
|  |         return text('OK') | ||||||
|  |  | ||||||
|  |     app.add_route(handler, '/test', methods=['GET']) | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, uri='/test') | ||||||
|  |     assert response.status == 200 | ||||||
|  |  | ||||||
|  |     request, response = sanic_endpoint_test(app, method='post', uri='/test') | ||||||
|  |     assert response.status == 405 | ||||||
|   | |||||||
							
								
								
									
										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