Compare commits
	
		
			63 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 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 | 
							
								
								
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| Version 0.1 | ||||
| ----------- | ||||
|  - 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. | ||||
| @@ -16,13 +16,14 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process.  E | ||||
|  | ||||
| | Server  | Implementation      | Requests/sec | Avg Latency | | ||||
| | ------- | ------------------- | ------------:| -----------:| | ||||
| | Sanic   | Python 3.5 + uvloop |       30,601 |      3.23ms | | ||||
| | Sanic   | Python 3.5 + uvloop |       33,342 |      2.96ms | | ||||
| | Wheezy  | gunicorn + meinheld |       20,244 |      4.94ms | | ||||
| | Falcon  | gunicorn + meinheld |       18,972 |      5.27ms | | ||||
| | Bottle  | gunicorn + meinheld |       13,596 |      7.36ms | | ||||
| | Flask   | gunicorn + meinheld |        4,988 |     20.08ms | | ||||
| | Kyoukai | Python 3.5 + uvloop |        3,889 |     27.44ms | | ||||
| | Aiohttp | Python 3.5 + uvloop |        2,979 |     33.42ms | | ||||
| | Tornado | Python 3.5          |        2,138 |     46.66ms | | ||||
|  | ||||
| ## Hello World | ||||
|  | ||||
| @@ -49,6 +50,7 @@ app.run(host="0.0.0.0", port=8000) | ||||
|  * [Middleware](docs/middleware.md) | ||||
|  * [Exceptions](docs/exceptions.md) | ||||
|  * [Blueprints](docs/blueprints.md) | ||||
|  * [Cookies](docs/cookies.md) | ||||
|  * [Deploying](docs/deploying.md) | ||||
|  * [Contributing](docs/contributing.md) | ||||
|  * [License](LICENSE) | ||||
|   | ||||
| @@ -29,7 +29,7 @@ from sanic import Blueprint | ||||
| bp = Blueprint('my_blueprint') | ||||
|  | ||||
| @bp.route('/') | ||||
| async def bp_root(): | ||||
| async def bp_root(request): | ||||
|     return json({'my': 'blueprint'}) | ||||
|  | ||||
| ``` | ||||
| @@ -80,3 +80,26 @@ Exceptions can also be applied exclusively to blueprints globally. | ||||
| def ignore_404s(request, exception): | ||||
| 	return text("Yep, I totally found the page: {}".format(request.url)) | ||||
| ``` | ||||
|  | ||||
| ## 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() | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										50
									
								
								docs/cookies.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								docs/cookies.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| # Cookies | ||||
|  | ||||
| ## Request | ||||
|  | ||||
| Request cookies can be accessed via the request.cookie dictionary | ||||
|  | ||||
| ### Example | ||||
|  | ||||
| ```python | ||||
| from sanic import Sanic | ||||
| from sanic.response import text | ||||
|  | ||||
| @app.route("/cookie") | ||||
| async def test(request): | ||||
|     test_cookie = request.cookies.get('test') | ||||
|     return text("Test cookie set to: {}".format(test_cookie)) | ||||
| ``` | ||||
|  | ||||
| ## Response | ||||
|  | ||||
| Response cookies can be set like dictionary values and  | ||||
| have the following parameters available: | ||||
|  | ||||
| * expires - datetime - Time for cookie to expire on the client's browser | ||||
| * path - string - The Path attribute specifies the subset of URLs to  | ||||
|          which this cookie applies | ||||
| * comment - string - Cookie comment (metadata) | ||||
| * domain - string - Specifies the domain for which the | ||||
|            cookie is valid.  An explicitly specified domain must always  | ||||
|            start with a dot. | ||||
| * max-age - number - Number of seconds the cookie should live for | ||||
| * secure - boolean - Specifies whether the cookie will only be sent via | ||||
|            HTTPS | ||||
| * httponly - boolean - Specifies whether the cookie cannot be read | ||||
|              by javascript | ||||
|  | ||||
| ### Example | ||||
|  | ||||
| ```python | ||||
| from sanic import Sanic | ||||
| from sanic.response import text | ||||
|  | ||||
| @app.route("/cookie") | ||||
| async def test(request): | ||||
|     response = text("There's a cookie up in this response") | ||||
|     response.cookies['test'] = 'It worked!' | ||||
|     response.cookies['test']['domain'] = '.gotta-go-fast.com' | ||||
|     response.cookies['test']['httponly'] = True | ||||
|     return response | ||||
| ``` | ||||
| @@ -8,6 +8,7 @@ The following request variables are accessible as properties: | ||||
| `request.json` (any) - JSON body   | ||||
| `request.args` (dict) - Query String variables.  Use getlist to get multiple of the same name   | ||||
| `request.form` (dict) - Posted form variables.  Use getlist to get multiple of the same name   | ||||
| `request.body` (bytes) - Posted raw body.  To get the raw data, regardless of content type   | ||||
|  | ||||
| See request.py for more information | ||||
|  | ||||
| @@ -15,7 +16,7 @@ See request.py for more information | ||||
|  | ||||
| ```python | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| from sanic.response import json, text | ||||
|  | ||||
| @app.route("/json") | ||||
| def post_json(request): | ||||
| @@ -40,4 +41,9 @@ def post_json(request): | ||||
| @app.route("/query_string") | ||||
| def query_string(request): | ||||
|     return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) | ||||
|  | ||||
|  | ||||
| @app.route("/users", methods=["POST",]) | ||||
| def create_user(request): | ||||
|     return text("You are trying to create a user with the following POST: %s" % request.body) | ||||
| ``` | ||||
|   | ||||
| @@ -10,16 +10,16 @@ from sanic import Sanic | ||||
| from sanic.response import text | ||||
|  | ||||
| @app.route('/tag/<tag>') | ||||
| async def person_handler(request, tag): | ||||
| async def tag_handler(request, tag): | ||||
| 	return text('Tag - {}'.format(tag)) | ||||
|  | ||||
| @app.route('/number/<integer_arg:int>') | ||||
| async def person_handler(request, integer_arg): | ||||
| async def integer_handler(request, integer_arg): | ||||
| 	return text('Integer - {}'.format(integer_arg)) | ||||
|  | ||||
| @app.route('/number/<number_arg:number>') | ||||
| async def person_handler(request, number_arg): | ||||
| 	return text('Number - {}'.format(number)) | ||||
| async def number_handler(request, number_arg): | ||||
| 	return text('Number - {}'.format(number_arg)) | ||||
|  | ||||
| @app.route('/person/<name:[A-z]>') | ||||
| async def person_handler(request, name): | ||||
|   | ||||
							
								
								
									
										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) | ||||
|  | ||||
| @@ -8,4 +8,5 @@ tox | ||||
| gunicorn | ||||
| bottle | ||||
| kyoukai | ||||
| falcon | ||||
| falcon | ||||
| tornado | ||||
| @@ -1,3 +1,6 @@ | ||||
| from collections import defaultdict | ||||
|  | ||||
|  | ||||
| class BlueprintSetup: | ||||
|     """ | ||||
|     """ | ||||
| @@ -22,7 +25,7 @@ class BlueprintSetup: | ||||
|         if self.url_prefix: | ||||
|             uri = self.url_prefix + uri | ||||
|  | ||||
|         self.app.router.add(uri, methods, handler) | ||||
|         self.app.route(uri=uri, methods=methods)(handler) | ||||
|  | ||||
|     def add_exception(self, handler, *args, **kwargs): | ||||
|         """ | ||||
| @@ -42,9 +45,15 @@ class BlueprintSetup: | ||||
|  | ||||
| class Blueprint: | ||||
|     def __init__(self, name, url_prefix=None): | ||||
|         """ | ||||
|         Creates a new blueprint | ||||
|         :param name: Unique name of the blueprint | ||||
|         :param url_prefix: URL to be prefixed before all route URLs | ||||
|         """ | ||||
|         self.name = name | ||||
|         self.url_prefix = url_prefix | ||||
|         self.deferred_functions = [] | ||||
|         self.listeners = defaultdict(list) | ||||
|  | ||||
|     def record(self, func): | ||||
|         """ | ||||
| @@ -73,6 +82,14 @@ class Blueprint: | ||||
|             return handler | ||||
|         return decorator | ||||
|  | ||||
|     def listener(self, event): | ||||
|         """ | ||||
|         """ | ||||
|         def decorator(listener): | ||||
|             self.listeners[event].append(listener) | ||||
|             return listener | ||||
|         return decorator | ||||
|  | ||||
|     def middleware(self, *args, **kwargs): | ||||
|         """ | ||||
|         """ | ||||
|   | ||||
| @@ -22,3 +22,4 @@ class Config: | ||||
| """ | ||||
|     REQUEST_MAX_SIZE = 100000000  # 100 megababies | ||||
|     REQUEST_TIMEOUT = 60  # 60 seconds | ||||
|     ROUTER_CACHE_SIZE = 1024 | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| from cgi import parse_header | ||||
| from collections import namedtuple | ||||
| from http.cookies import SimpleCookie | ||||
| from httptools import parse_url | ||||
| from urllib.parse import parse_qs | ||||
| from ujson import loads as json_loads | ||||
| @@ -30,7 +31,7 @@ class Request: | ||||
|     Properties of an HTTP request such as URL, headers, etc. | ||||
|     """ | ||||
|     __slots__ = ( | ||||
|         'url', 'headers', 'version', 'method', | ||||
|         'url', 'headers', 'version', 'method', '_cookies', | ||||
|         'query_string', 'body', | ||||
|         'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', | ||||
|     ) | ||||
| @@ -52,6 +53,7 @@ class Request: | ||||
|         self.parsed_form = None | ||||
|         self.parsed_files = None | ||||
|         self.parsed_args = None | ||||
|         self._cookies = None | ||||
|  | ||||
|     @property | ||||
|     def json(self): | ||||
| @@ -105,6 +107,18 @@ class Request: | ||||
|  | ||||
|         return self.parsed_args | ||||
|  | ||||
|     @property | ||||
|     def cookies(self): | ||||
|         if self._cookies is None: | ||||
|             if 'Cookie' in self.headers: | ||||
|                 cookies = SimpleCookie() | ||||
|                 cookies.load(self.headers['Cookie']) | ||||
|                 self._cookies = {name: cookie.value | ||||
|                                  for name, cookie in cookies.items()} | ||||
|             else: | ||||
|                 self._cookies = {} | ||||
|         return self._cookies | ||||
|  | ||||
|  | ||||
| File = namedtuple('File', ['type', 'body', 'name']) | ||||
|  | ||||
|   | ||||
| @@ -1,23 +1,76 @@ | ||||
| from datetime import datetime | ||||
| from http.cookies import SimpleCookie | ||||
| import ujson | ||||
|  | ||||
| STATUS_CODES = { | ||||
| COMMON_STATUS_CODES = { | ||||
|     200: b'OK', | ||||
|     400: b'Bad Request', | ||||
|     404: b'Not Found', | ||||
|     500: b'Internal Server Error', | ||||
| } | ||||
| ALL_STATUS_CODES = { | ||||
|     100: b'Continue', | ||||
|     101: b'Switching Protocols', | ||||
|     102: b'Processing', | ||||
|     200: b'OK', | ||||
|     201: b'Created', | ||||
|     202: b'Accepted', | ||||
|     203: b'Non-Authoritative Information', | ||||
|     204: b'No Content', | ||||
|     205: b'Reset Content', | ||||
|     206: b'Partial Content', | ||||
|     207: b'Multi-Status', | ||||
|     208: b'Already Reported', | ||||
|     226: b'IM Used', | ||||
|     300: b'Multiple Choices', | ||||
|     301: b'Moved Permanently', | ||||
|     302: b'Found', | ||||
|     303: b'See Other', | ||||
|     304: b'Not Modified', | ||||
|     305: b'Use Proxy', | ||||
|     307: b'Temporary Redirect', | ||||
|     308: b'Permanent Redirect', | ||||
|     400: b'Bad Request', | ||||
|     401: b'Unauthorized', | ||||
|     402: b'Payment Required', | ||||
|     403: b'Forbidden', | ||||
|     404: b'Not Found', | ||||
|     405: b'Method Not Allowed', | ||||
|     406: b'Not Acceptable', | ||||
|     407: b'Proxy Authentication Required', | ||||
|     408: b'Request Timeout', | ||||
|     409: b'Conflict', | ||||
|     410: b'Gone', | ||||
|     411: b'Length Required', | ||||
|     412: b'Precondition Failed', | ||||
|     413: b'Request Entity Too Large', | ||||
|     414: b'Request-URI Too Long', | ||||
|     415: b'Unsupported Media Type', | ||||
|     416: b'Requested Range Not Satisfiable', | ||||
|     417: b'Expectation Failed', | ||||
|     422: b'Unprocessable Entity', | ||||
|     423: b'Locked', | ||||
|     424: b'Failed Dependency', | ||||
|     426: b'Upgrade Required', | ||||
|     428: b'Precondition Required', | ||||
|     429: b'Too Many Requests', | ||||
|     431: b'Request Header Fields Too Large', | ||||
|     500: b'Internal Server Error', | ||||
|     501: b'Not Implemented', | ||||
|     502: b'Bad Gateway', | ||||
|     503: b'Service Unavailable', | ||||
|     504: b'Gateway Timeout', | ||||
|     505: b'HTTP Version Not Supported', | ||||
|     506: b'Variant Also Negotiates', | ||||
|     507: b'Insufficient Storage', | ||||
|     508: b'Loop Detected', | ||||
|     510: b'Not Extended', | ||||
|     511: b'Network Authentication Required' | ||||
| } | ||||
|  | ||||
|  | ||||
| class HTTPResponse: | ||||
|     __slots__ = ('body', 'status', 'content_type', 'headers') | ||||
|     __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') | ||||
|  | ||||
|     def __init__(self, body=None, status=200, headers=None, | ||||
|                  content_type='text/plain', body_bytes=b''): | ||||
| @@ -30,6 +83,7 @@ class HTTPResponse: | ||||
|  | ||||
|         self.status = status | ||||
|         self.headers = headers or {} | ||||
|         self._cookies = None | ||||
|  | ||||
|     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||
|         # This is all returned in a kind-of funky way | ||||
| @@ -44,6 +98,19 @@ class HTTPResponse: | ||||
|                 b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) | ||||
|                 for name, value in self.headers.items() | ||||
|             ) | ||||
|         if self._cookies: | ||||
|             for cookie in self._cookies.values(): | ||||
|                 if type(cookie['expires']) is datetime: | ||||
|                     cookie['expires'] = \ | ||||
|                         cookie['expires'].strftime("%a, %d-%b-%Y %T GMT") | ||||
|             headers += (str(self._cookies) + "\r\n").encode('utf-8') | ||||
|  | ||||
|         # Try to pull from the common codes first | ||||
|         # Speeds up response rate 6% over pulling from all | ||||
|         status = COMMON_STATUS_CODES.get(self.status) | ||||
|         if not status: | ||||
|             status = ALL_STATUS_CODES.get(self.status) | ||||
|  | ||||
|         return (b'HTTP/%b %d %b\r\n' | ||||
|                 b'Content-Type: %b\r\n' | ||||
|                 b'Content-Length: %d\r\n' | ||||
| @@ -52,7 +119,7 @@ class HTTPResponse: | ||||
|                 b'%b') % ( | ||||
|             version.encode(), | ||||
|             self.status, | ||||
|             STATUS_CODES.get(self.status, b'FAIL'), | ||||
|             status, | ||||
|             self.content_type.encode(), | ||||
|             len(self.body), | ||||
|             b'keep-alive' if keep_alive else b'close', | ||||
| @@ -61,10 +128,16 @@ class HTTPResponse: | ||||
|             self.body | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def cookies(self): | ||||
|         if self._cookies is None: | ||||
|             self._cookies = SimpleCookie() | ||||
|         return self._cookies | ||||
|  | ||||
|  | ||||
| def json(body, status=200, headers=None): | ||||
|     return HTTPResponse(ujson.dumps(body), headers=headers, status=status, | ||||
|                         content_type="application/json; charset=utf-8") | ||||
|                         content_type="application/json") | ||||
|  | ||||
|  | ||||
| def text(body, status=200, headers=None): | ||||
|   | ||||
							
								
								
									
										174
									
								
								sanic/router.py
									
									
									
									
									
								
							
							
						
						
									
										174
									
								
								sanic/router.py
									
									
									
									
									
								
							| @@ -1,9 +1,26 @@ | ||||
| import re | ||||
| from collections import namedtuple | ||||
| from collections import defaultdict, namedtuple | ||||
| from functools import lru_cache | ||||
| from .config import Config | ||||
| from .exceptions import NotFound, InvalidUsage | ||||
|  | ||||
| Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) | ||||
| Parameter = namedtuple("Parameter", ['name', 'cast']) | ||||
| Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) | ||||
| Parameter = namedtuple('Parameter', ['name', 'cast']) | ||||
|  | ||||
| REGEX_TYPES = { | ||||
|     'string': (str, r'[^/]+'), | ||||
|     'int': (int, r'\d+'), | ||||
|     'number': (float, r'[0-9\\.]+'), | ||||
|     'alpha': (str, r'[A-Za-z]+'), | ||||
| } | ||||
|  | ||||
|  | ||||
| def url_hash(url): | ||||
|     return url.count('/') | ||||
|  | ||||
|  | ||||
| class RouteExists(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class Router: | ||||
| @@ -18,22 +35,16 @@ class Router: | ||||
|     function provided Parameters can also have a type by appending :type to | ||||
|     the <parameter>.  If no type is provided, a string is expected.  A regular | ||||
|     expression can also be passed in as the type | ||||
|  | ||||
|     TODO: | ||||
|         This probably needs optimization for larger sets of routes, | ||||
|         since it checks every route until it finds a match which is bad and | ||||
|         I should feel bad | ||||
|     """ | ||||
|     routes = None | ||||
|     regex_types = { | ||||
|         "string": (None, "[^/]+"), | ||||
|         "int": (int, "\d+"), | ||||
|         "number": (float, "[0-9\\.]+"), | ||||
|         "alpha": (None, "[A-Za-z]+"), | ||||
|     } | ||||
|     routes_static = None | ||||
|     routes_dynamic = None | ||||
|     routes_always_check = None | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.routes = [] | ||||
|         self.routes_all = {} | ||||
|         self.routes_static = {} | ||||
|         self.routes_dynamic = defaultdict(list) | ||||
|         self.routes_always_check = [] | ||||
|  | ||||
|     def add(self, uri, methods, handler): | ||||
|         """ | ||||
| @@ -45,42 +56,49 @@ class Router: | ||||
|         When executed, it should provide a response object. | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         if uri in self.routes_all: | ||||
|             raise RouteExists("Route already registered: {}".format(uri)) | ||||
|  | ||||
|         # Dict for faster lookups of if method allowed | ||||
|         methods_dict = None | ||||
|         if methods: | ||||
|             methods_dict = {method: True for method in methods} | ||||
|             methods = frozenset(methods) | ||||
|  | ||||
|         parameters = [] | ||||
|         properties = {"unhashable": None} | ||||
|  | ||||
|         def add_parameter(match): | ||||
|             # We could receive NAME or NAME:PATTERN | ||||
|             parts = match.group(1).split(':') | ||||
|             if len(parts) == 2: | ||||
|                 parameter_name, parameter_pattern = parts | ||||
|             else: | ||||
|                 parameter_name = parts[0] | ||||
|                 parameter_pattern = 'string' | ||||
|             name = match.group(1) | ||||
|             pattern = 'string' | ||||
|             if ':' in name: | ||||
|                 name, pattern = name.split(':', 1) | ||||
|  | ||||
|             default = (str, pattern) | ||||
|             # Pull from pre-configured types | ||||
|             parameter_regex = self.regex_types.get(parameter_pattern) | ||||
|             if parameter_regex: | ||||
|                 parameter_type, parameter_pattern = parameter_regex | ||||
|             else: | ||||
|                 parameter_type = None | ||||
|  | ||||
|             parameter = Parameter(name=parameter_name, cast=parameter_type) | ||||
|             _type, pattern = REGEX_TYPES.get(pattern, default) | ||||
|             parameter = Parameter(name=name, cast=_type) | ||||
|             parameters.append(parameter) | ||||
|  | ||||
|             return "({})".format(parameter_pattern) | ||||
|             # Mark the whole route as unhashable if it has the hash key in it | ||||
|             if re.search('(^|[^^]){1}/', pattern): | ||||
|                 properties['unhashable'] = True | ||||
|  | ||||
|         pattern_string = re.sub("<(.+?)>", add_parameter, uri) | ||||
|         pattern = re.compile("^{}$".format(pattern_string)) | ||||
|             return '({})'.format(pattern) | ||||
|  | ||||
|         pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) | ||||
|         pattern = re.compile(r'^{}$'.format(pattern_string)) | ||||
|  | ||||
|         route = Route( | ||||
|             handler=handler, methods=methods_dict, pattern=pattern, | ||||
|             handler=handler, methods=methods, pattern=pattern, | ||||
|             parameters=parameters) | ||||
|         self.routes.append(route) | ||||
|  | ||||
|         self.routes_all[uri] = route | ||||
|         if properties['unhashable']: | ||||
|             self.routes_always_check.append(route) | ||||
|         elif parameters: | ||||
|             self.routes_dynamic[url_hash(uri)].append(route) | ||||
|         else: | ||||
|             self.routes_static[uri] = route | ||||
|  | ||||
|     def get(self, request): | ||||
|         """ | ||||
| @@ -89,58 +107,42 @@ class Router: | ||||
|         :param request: Request object | ||||
|         :return: handler, arguments, keyword arguments | ||||
|         """ | ||||
|         return self._get(request.url, request.method) | ||||
|  | ||||
|         route = None | ||||
|         args = [] | ||||
|         kwargs = {} | ||||
|         for _route in self.routes: | ||||
|             match = _route.pattern.match(request.url) | ||||
|             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 | ||||
|  | ||||
|     @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) | ||||
|     def _get(self, url, method): | ||||
|         """ | ||||
|         Gets a request handler based on the URL of the request, or raises an | ||||
|         error.  Internal method for caching. | ||||
|         :param url: Request URL | ||||
|         :param method: Request method | ||||
|         :return: handler, arguments, keyword arguments | ||||
|         """ | ||||
|         # Check against known static routes | ||||
|         route = self.routes_static.get(url) | ||||
|         if route: | ||||
|             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 | ||||
|             match = route.pattern.match(url) | ||||
|         else: | ||||
|             raise NotFound("Requested URL {} not found".format(request.url)) | ||||
|             # Move on to testing all regex routes | ||||
|             for route in self.routes_dynamic[url_hash(url)]: | ||||
|                 match = route.pattern.match(url) | ||||
|                 if match: | ||||
|                     break | ||||
|             else: | ||||
|                 # Lastly, check against all regex routes that cannot be hashed | ||||
|                 for route in self.routes_always_check: | ||||
|                     match = route.pattern.match(url) | ||||
|                     if match: | ||||
|                         break | ||||
|                 else: | ||||
|                     raise NotFound('Requested URL {} not found'.format(url)) | ||||
|  | ||||
|         if route.methods and method not in route.methods: | ||||
|             raise InvalidUsage( | ||||
|                 'Method {} not allowed for URL {}'.format( | ||||
|                     method, url), status_code=405) | ||||
|  | ||||
| class SimpleRouter: | ||||
|     """ | ||||
|     Simple router records and reads all routes from a dictionary | ||||
|     It does not support parameters in routes, but is very fast | ||||
|     """ | ||||
|     routes = None | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.routes = {} | ||||
|  | ||||
|     def add(self, uri, methods, handler): | ||||
|         # Dict for faster lookups of method allowed | ||||
|         methods_dict = None | ||||
|         if methods: | ||||
|             methods_dict = {method: True for method in methods} | ||||
|         self.routes[uri] = Route( | ||||
|             handler=handler, methods=methods_dict, pattern=uri, | ||||
|             parameters=None) | ||||
|  | ||||
|     def get(self, request): | ||||
|         route = self.routes.get(request.url) | ||||
|         if route: | ||||
|             if route.methods and request.method not in route.methods: | ||||
|                 raise InvalidUsage( | ||||
|                     "Method {} not allowed for URL {}".format( | ||||
|                         request.method, request.url), status_code=405) | ||||
|             return route.handler, [], {} | ||||
|         else: | ||||
|             raise NotFound("Requested URL {} not found".format(request.url)) | ||||
|         kwargs = {p.name: p.cast(value) | ||||
|                   for value, p | ||||
|                   in zip(match.groups(1), route.parameters)} | ||||
|         return route.handler, [], kwargs | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| from asyncio import get_event_loop | ||||
| from functools import partial | ||||
| from inspect import isawaitable | ||||
| from multiprocessing import Process, Event | ||||
| from signal import signal, SIGTERM, SIGINT | ||||
| @@ -24,6 +25,8 @@ class Sanic: | ||||
|         self.response_middleware = [] | ||||
|         self.blueprints = {} | ||||
|         self._blueprint_order = [] | ||||
|         self.loop = None | ||||
|         self.debug = None | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Registration | ||||
| @@ -47,9 +50,8 @@ class Sanic: | ||||
|     # Decorator | ||||
|     def exception(self, *exceptions): | ||||
|         """ | ||||
|         Decorates a function to be registered as a route | ||||
|         :param uri: path of the URL | ||||
|         :param methods: list or tuple of methods allowed | ||||
|         Decorates a function to be registered as a handler for exceptions | ||||
|         :param *exceptions: exceptions | ||||
|         :return: decorated function | ||||
|         """ | ||||
|  | ||||
| @@ -72,7 +74,7 @@ class Sanic: | ||||
|             if attach_to == 'request': | ||||
|                 self.request_middleware.append(middleware) | ||||
|             if attach_to == 'response': | ||||
|                 self.response_middleware.append(middleware) | ||||
|                 self.response_middleware.insert(0, middleware) | ||||
|             return middleware | ||||
|  | ||||
|         # Detect which way this was called, @middleware or @middleware('AT') | ||||
| @@ -103,6 +105,9 @@ class Sanic: | ||||
|     # Request Handling | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     def converted_response_type(self, response): | ||||
|         pass | ||||
|  | ||||
|     async def handle_request(self, request, response_callback): | ||||
|         """ | ||||
|         Takes a request from the HTTP Server and returns a response object to | ||||
| @@ -114,7 +119,10 @@ class Sanic: | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         try: | ||||
|             # Middleware process_request | ||||
|             # -------------------------------------------- # | ||||
|             # Request Middleware | ||||
|             # -------------------------------------------- # | ||||
|  | ||||
|             response = False | ||||
|             # The if improves speed.  I don't know why | ||||
|             if self.request_middleware: | ||||
| @@ -127,6 +135,10 @@ class Sanic: | ||||
|  | ||||
|             # No middleware results | ||||
|             if not response: | ||||
|                 # -------------------------------------------- # | ||||
|                 # Execute Handler | ||||
|                 # -------------------------------------------- # | ||||
|  | ||||
|                 # Fetch handler from router | ||||
|                 handler, args, kwargs = self.router.get(request) | ||||
|                 if handler is None: | ||||
| @@ -139,7 +151,10 @@ class Sanic: | ||||
|                 if isawaitable(response): | ||||
|                     response = await response | ||||
|  | ||||
|                 # Middleware process_response | ||||
|                 # -------------------------------------------- # | ||||
|                 # Response Middleware | ||||
|                 # -------------------------------------------- # | ||||
|  | ||||
|                 if self.response_middleware: | ||||
|                     for middleware in self.response_middleware: | ||||
|                         _response = middleware(request, response) | ||||
| @@ -150,6 +165,10 @@ class Sanic: | ||||
|                             break | ||||
|  | ||||
|         except Exception as e: | ||||
|             # -------------------------------------------- # | ||||
|             # Response Generation Failed | ||||
|             # -------------------------------------------- # | ||||
|  | ||||
|             try: | ||||
|                 response = self.error_handler.response(request, e) | ||||
|                 if isawaitable(response): | ||||
| @@ -169,25 +188,32 @@ class Sanic: | ||||
|     # Execution | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, | ||||
|             before_stop=None, sock=None, workers=1): | ||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, | ||||
|             after_start=None, before_stop=None, after_stop=None, sock=None, | ||||
|             workers=1, loop=None): | ||||
|         """ | ||||
|         Runs the HTTP Server and listens until keyboard interrupt or term | ||||
|         signal. On termination, drains connections before closing. | ||||
|         :param host: Address to host on | ||||
|         :param port: Port to host on | ||||
|         :param debug: Enables debug output (slows server) | ||||
|         :param before_start: Function to be executed before the server starts | ||||
|         accepting connections | ||||
|         :param after_start: Function to be executed after the server starts | ||||
|         listening | ||||
|         accepting connections | ||||
|         :param before_stop: Function to be executed when a stop signal is | ||||
|         received before it is respected | ||||
|         :param after_stop: Function to be executed when all requests are | ||||
|         complete | ||||
|         :param sock: Socket for the server to accept connections from | ||||
|         :param workers: Number of processes | ||||
|         received before it is respected | ||||
|         :param loop: asyncio compatible event loop | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         self.error_handler.debug = True | ||||
|         self.debug = debug | ||||
|         self.loop = loop | ||||
|  | ||||
|         server_settings = { | ||||
|             'host': host, | ||||
| @@ -197,8 +223,32 @@ class Sanic: | ||||
|             'request_handler': self.handle_request, | ||||
|             'request_timeout': self.config.REQUEST_TIMEOUT, | ||||
|             'request_max_size': self.config.REQUEST_MAX_SIZE, | ||||
|             'loop': loop | ||||
|         } | ||||
|  | ||||
|         # -------------------------------------------- # | ||||
|         # Register start/stop events | ||||
|         # -------------------------------------------- # | ||||
|  | ||||
|         for event_name, settings_name, args, reverse in ( | ||||
|                 ("before_server_start", "before_start", before_start, False), | ||||
|                 ("after_server_start", "after_start", after_start, False), | ||||
|                 ("before_server_stop", "before_stop", before_stop, True), | ||||
|                 ("after_server_stop", "after_stop", after_stop, True), | ||||
|                 ): | ||||
|             listeners = [] | ||||
|             for blueprint in self.blueprints.values(): | ||||
|                 listeners += blueprint.listeners[event_name] | ||||
|             if args: | ||||
|                 if type(args) is not list: | ||||
|                     args = [args] | ||||
|                 listeners += args | ||||
|             if reverse: | ||||
|                 listeners.reverse() | ||||
|             # Prepend sanic to the arguments when listeners are triggered | ||||
|             listeners = [partial(listener, self) for listener in listeners] | ||||
|             server_settings[settings_name] = listeners | ||||
|  | ||||
|         if debug: | ||||
|             log.setLevel(logging.DEBUG) | ||||
|         log.debug(self.config.LOGO) | ||||
| @@ -208,8 +258,6 @@ class Sanic: | ||||
|  | ||||
|         try: | ||||
|             if workers == 1: | ||||
|                 server_settings['after_start'] = after_start | ||||
|                 server_settings['before_stop'] = before_stop | ||||
|                 serve(**server_settings) | ||||
|             else: | ||||
|                 log.info('Spinning up {} workers...'.format(workers)) | ||||
|   | ||||
| @@ -110,7 +110,10 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         ) | ||||
|  | ||||
|     def on_body(self, body): | ||||
|         self.request.body = body | ||||
|         if self.request.body: | ||||
|             self.request.body += body | ||||
|         else: | ||||
|             self.request.body = body | ||||
|  | ||||
|     def on_message_complete(self): | ||||
|         self.loop.create_task( | ||||
| @@ -157,15 +160,48 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
| 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, before_start=None, after_start=None, | ||||
|           before_stop=None, after_stop=None, | ||||
|           debug=False, request_timeout=60, sock=None, | ||||
|           request_max_size=None, reuse_port=False): | ||||
|     # Create Event Loop | ||||
|     loop = async_loop.new_event_loop() | ||||
|           request_max_size=None, reuse_port=False, loop=None): | ||||
|     """ | ||||
|     Starts asynchronous HTTP Server on an individual process. | ||||
|     :param host: Address to host on | ||||
|     :param port: Port to host on | ||||
|     :param request_handler: Sanic request handler with middleware | ||||
|     :param after_start: Function to be executed after the server starts | ||||
|     listening. Takes single argument `loop` | ||||
|     :param before_stop: Function to be executed when a stop signal is | ||||
|     received before it is respected. Takes single argumenet `loop` | ||||
|     :param debug: Enables debug output (slows server) | ||||
|     :param request_timeout: time in seconds | ||||
|     :param sock: Socket for the server to accept connections from | ||||
|     :param request_max_size: size in bytes, `None` for no limit | ||||
|     :param reuse_port: `True` for multiple workers | ||||
|     :param loop: asyncio compatible event loop | ||||
|     :return: Nothing | ||||
|     """ | ||||
|     loop = loop or async_loop.new_event_loop() | ||||
|     asyncio.set_event_loop(loop) | ||||
|     # I don't think we take advantage of this | ||||
|     # And it slows everything waaayyy down | ||||
|     # loop.set_debug(debug) | ||||
|  | ||||
|     if debug: | ||||
|         loop.set_debug(debug) | ||||
|  | ||||
|     trigger_events(before_start, loop) | ||||
|  | ||||
|     connections = {} | ||||
|     signal = Signal() | ||||
| @@ -177,17 +213,14 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
|         request_timeout=request_timeout, | ||||
|         request_max_size=request_max_size, | ||||
|     ), host, port, reuse_port=reuse_port, sock=sock) | ||||
|  | ||||
|     try: | ||||
|         http_server = loop.run_until_complete(server_coroutine) | ||||
|     except Exception as e: | ||||
|         log.error("Unable to start server: {}".format(e)) | ||||
|         log.exception("Unable to start server") | ||||
|         return | ||||
|  | ||||
|     # Run the on_start function if provided | ||||
|     if after_start: | ||||
|         result = after_start(loop) | ||||
|         if isawaitable(result): | ||||
|             loop.run_until_complete(result) | ||||
|     trigger_events(after_start, loop) | ||||
|  | ||||
|     # Register signals for graceful termination | ||||
|     for _signal in (SIGINT, SIGTERM): | ||||
| @@ -199,10 +232,7 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
|         log.info("Stop requested, draining connections...") | ||||
|  | ||||
|         # Run the on_stop function if provided | ||||
|         if before_stop: | ||||
|             result = before_stop(loop) | ||||
|             if isawaitable(result): | ||||
|                 loop.run_until_complete(result) | ||||
|         trigger_events(before_stop, loop) | ||||
|  | ||||
|         # Wait for event loop to finish and all connections to drain | ||||
|         http_server.close() | ||||
| @@ -216,4 +246,6 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
|         while connections: | ||||
|             loop.run_until_complete(asyncio.sleep(0.1)) | ||||
|  | ||||
|         trigger_events(after_stop, loop) | ||||
|  | ||||
|         loop.close() | ||||
|   | ||||
| @@ -5,10 +5,10 @@ HOST = '127.0.0.1' | ||||
| PORT = 42101 | ||||
|  | ||||
|  | ||||
| async def local_request(method, uri, *args, **kwargs): | ||||
| async def local_request(method, uri, cookies=None, *args, **kwargs): | ||||
|     url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) | ||||
|     log.info(url) | ||||
|     async with aiohttp.ClientSession() as session: | ||||
|     async with aiohttp.ClientSession(cookies=cookies) as session: | ||||
|         async with getattr(session, method)(url, *args, **kwargs) as response: | ||||
|             response.text = await response.text() | ||||
|             return response | ||||
| @@ -24,7 +24,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||
|         def _collect_request(request): | ||||
|             results.append(request) | ||||
|  | ||||
|     async def _collect_response(loop): | ||||
|     async def _collect_response(sanic, loop): | ||||
|         try: | ||||
|             response = await local_request(method, uri, *request_args, | ||||
|                                            **request_kwargs) | ||||
|   | ||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -5,7 +5,7 @@ from setuptools import setup | ||||
|  | ||||
| setup( | ||||
|     name='Sanic', | ||||
|     version="0.1.4", | ||||
|     version="0.1.5", | ||||
|     url='http://github.com/channelcat/sanic/', | ||||
|     license='MIT', | ||||
|     author='Channel Cat', | ||||
|   | ||||
							
								
								
									
										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() | ||||
							
								
								
									
										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() | ||||
| @@ -108,4 +108,40 @@ def test_bp_exception_handler(): | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|     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.register_blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/') | ||||
|  | ||||
|     assert order == [1,2,3,4,5,6] | ||||
							
								
								
									
										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 | ||||
| @@ -80,3 +80,38 @@ def test_post_json(): | ||||
|  | ||||
|     assert request.json.get('test') == 'OK' | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|  | ||||
| def test_post_form_urlencoded(): | ||||
|     app = Sanic('test_post_form_urlencoded') | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     payload = 'test=OK' | ||||
|     headers = {'content-type': 'application/x-www-form-urlencoded'} | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, data=payload, headers=headers) | ||||
|  | ||||
|     assert request.form.get('test') == 'OK' | ||||
|  | ||||
|  | ||||
| def test_post_form_multipart_form_data(): | ||||
|     app = Sanic('test_post_form_multipart_form_data') | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     payload = '------sanic\r\n' \ | ||||
|               'Content-Disposition: form-data; name="test"\r\n' \ | ||||
|               '\r\n' \ | ||||
|               'OK\r\n' \ | ||||
|               '------sanic--\r\n' | ||||
|  | ||||
|     headers = {'content-type': 'multipart/form-data; boundary=----sanic'} | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, data=payload, headers=headers) | ||||
|  | ||||
|     assert request.form.get('test') == 'OK' | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| from json import loads as json_loads, dumps as json_dumps | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import json, text | ||||
| from sanic.response import text | ||||
| from sanic.router import RouteExists | ||||
| from sanic.utils import sanic_endpoint_test | ||||
|  | ||||
|  | ||||
| @@ -8,6 +10,24 @@ from sanic.utils import sanic_endpoint_test | ||||
| #  UTF-8 | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| def test_static_routes(): | ||||
|     app = Sanic('test_dynamic_route') | ||||
|  | ||||
|     @app.route('/test') | ||||
|     async def handler1(request): | ||||
|         return text('OK1') | ||||
|  | ||||
|     @app.route('/pizazz') | ||||
|     async def handler2(request): | ||||
|         return text('OK2') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/test') | ||||
|     assert response.text == 'OK1' | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/pizazz') | ||||
|     assert response.text == 'OK2' | ||||
|  | ||||
|  | ||||
| def test_dynamic_route(): | ||||
|     app = Sanic('test_dynamic_route') | ||||
|  | ||||
| @@ -102,3 +122,45 @@ def test_dynamic_route_regex(): | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/folder/') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_dynamic_route_unhashable(): | ||||
|     app = Sanic('test_dynamic_route_unhashable') | ||||
|  | ||||
|     @app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/') | ||||
|     async def handler(request, unhashable): | ||||
|         return text('OK') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/folder/test/end/') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/folder/test/nope/') | ||||
|     assert response.status == 404 | ||||
|  | ||||
|  | ||||
| def test_route_duplicate(): | ||||
|     app = Sanic('test_dynamic_route') | ||||
|  | ||||
|     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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user