22
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								CHANGELOG.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| Version 0.1 | ||||
| ----------- | ||||
|  - 0.1.7 | ||||
|   - Reversed static url and directory arguments to meet spec | ||||
|  - 0.1.6 | ||||
|   - Static files | ||||
|   - Lazy Cookie Loading | ||||
|  - 0.1.5  | ||||
|   - Cookies | ||||
|   - Blueprint listeners and ordering | ||||
|   - Faster Router | ||||
|   - Fix: Incomplete file reads on medium+ sized post requests | ||||
|   - Breaking: after_start and before_stop now pass sanic as their first argument | ||||
|  - 0.1.4  | ||||
|   - Multiprocessing | ||||
|  - 0.1.3 | ||||
|   - Blueprint support | ||||
|   - Faster Response processing | ||||
|  - 0.1.1 - 0.1.2  | ||||
|   - Struggling to update pypi via CI | ||||
|  - 0.1.0  | ||||
|   - Released to public | ||||
							
								
								
									
										14
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								README.md
									
									
									
									
									
								
							| @@ -4,23 +4,26 @@ | ||||
| [](https://pypi.python.org/pypi/sanic/) | ||||
| [](https://pypi.python.org/pypi/sanic/) | ||||
|  | ||||
| Sanic is a Flask-like Python 3.5+ web server that's written to go fast.  It's based off the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/. | ||||
| Sanic is a Flask-like Python 3.5+ web server that's written to go fast.  It's based on the work done by the amazing folks at magicstack, and was inspired by this article: https://magic.io/blog/uvloop-blazing-fast-python-networking/. | ||||
|  | ||||
| On top of being flask-like, sanic supports async request handlers.  This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. | ||||
| On top of being Flask-like, Sanic supports async request handlers.  This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. | ||||
|  | ||||
| ## Benchmarks | ||||
|  | ||||
| All tests were run on a AWS medium instance running ubuntu, using 1 process.  Each script delivered a small JSON response and was tested with wrk using 100 connections.  Pypy was tested for falcon and flask, but did not speed up requests. | ||||
| All tests were run on an AWS medium instance running ubuntu, using 1 process.  Each script delivered a small JSON response and was tested with wrk using 100 connections.  Pypy was tested for Falcon and Flask but did not speed up requests. | ||||
|  | ||||
|  | ||||
|  | ||||
| | 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 | ||||
|  | ||||
| @@ -47,6 +50,9 @@ 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) | ||||
|  * [Static Files](docs/static_files.md) | ||||
|  * [Deploying](docs/deploying.md) | ||||
|  * [Contributing](docs/contributing.md) | ||||
|  * [License](LICENSE) | ||||
|  | ||||
|   | ||||
| @@ -29,7 +29,7 @@ from sanic import Blueprint | ||||
| bp = Blueprint('my_blueprint') | ||||
|  | ||||
| @bp.route('/') | ||||
| async def bp_root(): | ||||
| async def bp_root(request): | ||||
|     return json({'my': 'blueprint'}) | ||||
|  | ||||
| ``` | ||||
| @@ -42,7 +42,7 @@ from sanic import Sanic | ||||
| from my_blueprint import bp | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app.register_blueprint(bp) | ||||
| app.blueprint(bp) | ||||
|  | ||||
| app.run(host='0.0.0.0', port=8000, debug=True) | ||||
| ``` | ||||
| @@ -79,4 +79,33 @@ Exceptions can also be applied exclusively to blueprints globally. | ||||
| @bp.exception(NotFound) | ||||
| def ignore_404s(request, exception): | ||||
| 	return text("Yep, I totally found the page: {}".format(request.url)) | ||||
|  | ||||
| ## Static files | ||||
| Static files can also be served globally, under the blueprint prefix. | ||||
|  | ||||
| ```python | ||||
| bp.static('/folder/to/serve', '/web/path') | ||||
| ``` | ||||
|  | ||||
| ## Start and Stop | ||||
| Blueprints and run functions during the start and stop process of the server. | ||||
| If running in multiprocessor mode (more than 1 worker), these are triggered after the workers fork | ||||
| Available events are: | ||||
|  | ||||
|  * before_server_start - Executed before the server begins to accept connections | ||||
|  * after_server_start - Executed after the server begins to accept connections | ||||
|  * before_server_stop - Executed before the server stops accepting connections | ||||
|  * after_server_stop - Executed after the server is stopped and all requests are complete | ||||
|  | ||||
| ```python | ||||
| bp = Blueprint('my_blueprint') | ||||
|  | ||||
| @bp.listen('before_server_start') | ||||
| async def setup_connection(): | ||||
|     global database | ||||
|     database = mysql.connect(host='127.0.0.1'...) | ||||
|      | ||||
| @bp.listen('after_server_stop') | ||||
| async def close_connection(): | ||||
|     await database.close() | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										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 | ||||
| ``` | ||||
							
								
								
									
										35
									
								
								docs/deploying.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/deploying.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| # Deploying | ||||
|  | ||||
| When it comes to deploying Sanic, there's not much to it, but there are | ||||
| a few things to take note of. | ||||
|  | ||||
| ## Workers | ||||
|  | ||||
| By default, Sanic listens in the main process using only 1 CPU core. | ||||
| To crank up the juice, just specify the number of workers in the run | ||||
| arguments like so: | ||||
|  | ||||
| ```python | ||||
| app.run(host='0.0.0.0', port=1337, workers=4) | ||||
| ``` | ||||
|  | ||||
| Sanic will automatically spin up multiple processes and route | ||||
| traffic between them.  We recommend as many workers as you have | ||||
| available cores. | ||||
|  | ||||
| ## Running via Command | ||||
|  | ||||
| If you like using command line arguments, you can launch a sanic server | ||||
| by executing the module.  For example, if you initialized sanic as | ||||
| app in a file named server.py, you could run the server like so: | ||||
|  | ||||
| `python -m sanic server.app --host=0.0.0.0 --port=1337 --workers=4` | ||||
|  | ||||
| With this way of running sanic, it is not necessary to run app.run in | ||||
| your python file.  If you do, just make sure you wrap it in name == main | ||||
| like so: | ||||
|  | ||||
| ```python | ||||
| if __name__ == '__main__': | ||||
|     app.run(host='0.0.0.0', port=1337, workers=4) | ||||
| ``` | ||||
| @@ -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): | ||||
|   | ||||
							
								
								
									
										18
									
								
								docs/static_files.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								docs/static_files.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| # Static Files | ||||
|  | ||||
| Both directories and files can be served by registering with static | ||||
|  | ||||
| ## Example | ||||
|  | ||||
| ```python | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| # Serves files from the static folder to the URL /static | ||||
| app.static('/static', './static') | ||||
|  | ||||
| # Serves the file /home/ubuntu/test.png when the URL /the_best.png | ||||
| # is requested | ||||
| app.static('/the_best.png', '/home/ubuntu/test.png') | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000) | ||||
| ``` | ||||
							
								
								
									
										33
									
								
								examples/aiohttp_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								examples/aiohttp_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
|  | ||||
| import uvloop | ||||
| import aiohttp | ||||
|  | ||||
| #Create an event loop manually so that we can use it for both sanic & aiohttp | ||||
| loop = uvloop.new_event_loop() | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| async def fetch(session, url): | ||||
|     """ | ||||
|     Use session object to perform 'get' request on url | ||||
|     """ | ||||
|     async with session.get(url) as response: | ||||
|         return await response.json() | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| async def test(request): | ||||
|     """ | ||||
|     Download and serve example JSON | ||||
|     """ | ||||
|     url = "https://api.github.com/repos/channelcat/sanic" | ||||
|  | ||||
|     async with aiohttp.ClientSession(loop=loop) as session: | ||||
|         response = await fetch(session, url) | ||||
|         return json(response) | ||||
|  | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000, loop=loop) | ||||
|  | ||||
							
								
								
									
										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,3 +8,5 @@ tox | ||||
| gunicorn | ||||
| bottle | ||||
| kyoukai | ||||
| falcon | ||||
| tornado | ||||
| @@ -1,4 +1,6 @@ | ||||
| from .sanic import Sanic | ||||
| from .blueprints import Blueprint | ||||
|  | ||||
| __version__ = '0.1.7' | ||||
|  | ||||
| __all__ = ['Sanic', 'Blueprint'] | ||||
|   | ||||
							
								
								
									
										36
									
								
								sanic/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								sanic/__main__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| from argparse import ArgumentParser | ||||
| from importlib import import_module | ||||
|  | ||||
| from .log import log | ||||
| from .sanic import Sanic | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     parser = ArgumentParser(prog='sanic') | ||||
|     parser.add_argument('--host', dest='host', type=str, default='127.0.0.1') | ||||
|     parser.add_argument('--port', dest='port', type=int, default=8000) | ||||
|     parser.add_argument('--workers', dest='workers', type=int, default=1, ) | ||||
|     parser.add_argument('--debug', dest='debug', action="store_true") | ||||
|     parser.add_argument('module') | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     try: | ||||
|         module_parts = args.module.split(".") | ||||
|         module_name = ".".join(module_parts[:-1]) | ||||
|         app_name = module_parts[-1] | ||||
|  | ||||
|         module = import_module(module_name) | ||||
|         app = getattr(module, app_name, None) | ||||
|         if type(app) is not Sanic: | ||||
|             raise ValueError("Module is not a Sanic app, it is a {}.  " | ||||
|                              "Perhaps you meant {}.app?" | ||||
|                              .format(type(app).__name__, args.module)) | ||||
|  | ||||
|         app.run(host=args.host, port=args.port, | ||||
|                 workers=args.workers, debug=args.debug) | ||||
|     except ImportError: | ||||
|         log.error("No module named {} found.\n" | ||||
|                   "  Example File: project/sanic_server.py -> app\n" | ||||
|                   "  Example Module: project.sanic_server.app" | ||||
|                   .format(module_name)) | ||||
|     except ValueError as e: | ||||
|         log.error("{}".format(e)) | ||||
| @@ -1,3 +1,6 @@ | ||||
| from collections import defaultdict | ||||
|  | ||||
|  | ||||
| class BlueprintSetup: | ||||
|     """ | ||||
|     """ | ||||
| @@ -22,7 +25,7 @@ class BlueprintSetup: | ||||
|         if self.url_prefix: | ||||
|             uri = self.url_prefix + uri | ||||
|  | ||||
|         self.app.router.add(uri, methods, handler) | ||||
|         self.app.route(uri=uri, methods=methods)(handler) | ||||
|  | ||||
|     def add_exception(self, handler, *args, **kwargs): | ||||
|         """ | ||||
| @@ -30,6 +33,15 @@ class BlueprintSetup: | ||||
|         """ | ||||
|         self.app.exception(*args, **kwargs)(handler) | ||||
|  | ||||
|     def add_static(self, uri, file_or_directory, *args, **kwargs): | ||||
|         """ | ||||
|         Registers static files to sanic | ||||
|         """ | ||||
|         if self.url_prefix: | ||||
|             uri = self.url_prefix + uri | ||||
|  | ||||
|         self.app.static(uri, file_or_directory, *args, **kwargs) | ||||
|  | ||||
|     def add_middleware(self, middleware, *args, **kwargs): | ||||
|         """ | ||||
|         Registers middleware to sanic | ||||
| @@ -42,9 +54,15 @@ class BlueprintSetup: | ||||
|  | ||||
| class Blueprint: | ||||
|     def __init__(self, name, url_prefix=None): | ||||
|         """ | ||||
|         Creates a new blueprint | ||||
|         :param name: Unique name of the blueprint | ||||
|         :param url_prefix: URL to be prefixed before all route URLs | ||||
|         """ | ||||
|         self.name = name | ||||
|         self.url_prefix = url_prefix | ||||
|         self.deferred_functions = [] | ||||
|         self.listeners = defaultdict(list) | ||||
|  | ||||
|     def record(self, func): | ||||
|         """ | ||||
| @@ -73,6 +91,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): | ||||
|         """ | ||||
|         """ | ||||
| @@ -95,3 +121,9 @@ class Blueprint: | ||||
|             self.record(lambda s: s.add_exception(handler, *args, **kwargs)) | ||||
|             return handler | ||||
|         return decorator | ||||
|  | ||||
|     def static(self, uri, file_or_directory, *args, **kwargs): | ||||
|         """ | ||||
|         """ | ||||
|         self.record( | ||||
|             lambda s: s.add_static(uri, file_or_directory, *args, **kwargs)) | ||||
|   | ||||
| @@ -22,3 +22,4 @@ class Config: | ||||
| """ | ||||
|     REQUEST_MAX_SIZE = 100000000  # 100 megababies | ||||
|     REQUEST_TIMEOUT = 60  # 60 seconds | ||||
|     ROUTER_CACHE_SIZE = 1024 | ||||
|   | ||||
							
								
								
									
										129
									
								
								sanic/cookies.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								sanic/cookies.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| from datetime import datetime | ||||
| import re | ||||
| import string | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  SimpleCookie | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| # Straight up copied this section of dark magic from SimpleCookie | ||||
|  | ||||
| _LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" | ||||
| _UnescapedChars = _LegalChars + ' ()/<=>?@[]{}' | ||||
|  | ||||
| _Translator = {n: '\\%03o' % n | ||||
|                for n in set(range(256)) - set(map(ord, _UnescapedChars))} | ||||
| _Translator.update({ | ||||
|     ord('"'): '\\"', | ||||
|     ord('\\'): '\\\\', | ||||
| }) | ||||
|  | ||||
|  | ||||
| def _quote(str): | ||||
|     r"""Quote a string for use in a cookie header. | ||||
|     If the string does not need to be double-quoted, then just return the | ||||
|     string.  Otherwise, surround the string in doublequotes and quote | ||||
|     (with a \) special characters. | ||||
|     """ | ||||
|     if str is None or _is_legal_key(str): | ||||
|         return str | ||||
|     else: | ||||
|         return '"' + str.translate(_Translator) + '"' | ||||
|  | ||||
| _is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  Custom SimpleCookie | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
|  | ||||
| class CookieJar(dict): | ||||
|     """ | ||||
|     CookieJar dynamically writes headers as cookies are added and removed | ||||
|     It gets around the limitation of one header per name by using the | ||||
|     MultiHeader class to provide a unique key that encodes to Set-Cookie | ||||
|     """ | ||||
|     def __init__(self, headers): | ||||
|         super().__init__() | ||||
|         self.headers = headers | ||||
|         self.cookie_headers = {} | ||||
|  | ||||
|     def __setitem__(self, key, value): | ||||
|         # If this cookie doesn't exist, add it to the header keys | ||||
|         cookie_header = self.cookie_headers.get(key) | ||||
|         if not cookie_header: | ||||
|             cookie = Cookie(key, value) | ||||
|             cookie_header = MultiHeader("Set-Cookie") | ||||
|             self.cookie_headers[key] = cookie_header | ||||
|             self.headers[cookie_header] = cookie | ||||
|             return super().__setitem__(key, cookie) | ||||
|         else: | ||||
|             self[key].value = value | ||||
|  | ||||
|     def __delitem__(self, key): | ||||
|         del self.cookie_headers[key] | ||||
|         return super().__delitem__(key) | ||||
|  | ||||
|  | ||||
| class Cookie(dict): | ||||
|     """ | ||||
|     This is a stripped down version of Morsel from SimpleCookie #gottagofast | ||||
|     """ | ||||
|     _keys = { | ||||
|         "expires": "expires", | ||||
|         "path": "Path", | ||||
|         "comment": "Comment", | ||||
|         "domain": "Domain", | ||||
|         "max-age": "Max-Age", | ||||
|         "secure": "Secure", | ||||
|         "httponly": "HttpOnly", | ||||
|         "version": "Version", | ||||
|     } | ||||
|     _flags = {'secure', 'httponly'} | ||||
|  | ||||
|     def __init__(self, key, value): | ||||
|         if key in self._keys: | ||||
|             raise KeyError("Cookie name is a reserved word") | ||||
|         if not _is_legal_key(key): | ||||
|             raise KeyError("Cookie key contains illegal characters") | ||||
|         self.key = key | ||||
|         self.value = value | ||||
|         super().__init__() | ||||
|  | ||||
|     def __setitem__(self, key, value): | ||||
|         if key not in self._keys: | ||||
|             raise KeyError("Unknown cookie property") | ||||
|         return super().__setitem__(key, value) | ||||
|  | ||||
|     def encode(self, encoding): | ||||
|         output = ['%s=%s' % (self.key, _quote(self.value))] | ||||
|         for key, value in self.items(): | ||||
|             if key == 'max-age' and isinstance(value, int): | ||||
|                 output.append('%s=%d' % (self._keys[key], value)) | ||||
|             elif key == 'expires' and isinstance(value, datetime): | ||||
|                 output.append('%s=%s' % ( | ||||
|                     self._keys[key], | ||||
|                     value.strftime("%a, %d-%b-%Y %T GMT") | ||||
|                 )) | ||||
|             elif key in self._flags: | ||||
|                 output.append(self._keys[key]) | ||||
|             else: | ||||
|                 output.append('%s=%s' % (self._keys[key], value)) | ||||
|  | ||||
|         return "; ".join(output).encode(encoding) | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  Header Trickery | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
|  | ||||
| class MultiHeader: | ||||
|     """ | ||||
|     Allows us to set a header within response that has a unique key, | ||||
|     but may contain duplicate header names | ||||
|     """ | ||||
|     def __init__(self, name): | ||||
|         self.name = name | ||||
|  | ||||
|     def encode(self): | ||||
|         return self.name.encode() | ||||
| @@ -21,6 +21,15 @@ class ServerError(SanicException): | ||||
|     status_code = 500 | ||||
|  | ||||
|  | ||||
| class FileNotFound(NotFound): | ||||
|     status_code = 404 | ||||
|  | ||||
|     def __init__(self, message, path, relative_url): | ||||
|         super().__init__(message) | ||||
|         self.path = path | ||||
|         self.relative_url = relative_url | ||||
|  | ||||
|  | ||||
| class Handler: | ||||
|     handlers = None | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| from cgi import parse_header | ||||
| from collections import namedtuple | ||||
| from http.cookies import SimpleCookie | ||||
| from httptools import parse_url | ||||
| from urllib.parse import parse_qs | ||||
| from ujson import loads as json_loads | ||||
| @@ -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,78 @@ | ||||
| import ujson | ||||
| from aiofiles import open as open_async | ||||
| from .cookies import CookieJar | ||||
| from mimetypes import guess_type | ||||
| from os import path | ||||
| from ujson import dumps as json_dumps | ||||
|  | ||||
| STATUS_CODES = { | ||||
| COMMON_STATUS_CODES = { | ||||
|     200: b'OK', | ||||
|     400: b'Bad Request', | ||||
|     404: b'Not Found', | ||||
|     500: b'Internal Server Error', | ||||
| } | ||||
| ALL_STATUS_CODES = { | ||||
|     100: b'Continue', | ||||
|     101: b'Switching Protocols', | ||||
|     102: b'Processing', | ||||
|     200: b'OK', | ||||
|     201: b'Created', | ||||
|     202: b'Accepted', | ||||
|     203: b'Non-Authoritative Information', | ||||
|     204: b'No Content', | ||||
|     205: b'Reset Content', | ||||
|     206: b'Partial Content', | ||||
|     207: b'Multi-Status', | ||||
|     208: b'Already Reported', | ||||
|     226: b'IM Used', | ||||
|     300: b'Multiple Choices', | ||||
|     301: b'Moved Permanently', | ||||
|     302: b'Found', | ||||
|     303: b'See Other', | ||||
|     304: b'Not Modified', | ||||
|     305: b'Use Proxy', | ||||
|     307: b'Temporary Redirect', | ||||
|     308: b'Permanent Redirect', | ||||
|     400: b'Bad Request', | ||||
|     401: b'Unauthorized', | ||||
|     402: b'Payment Required', | ||||
|     403: b'Forbidden', | ||||
|     404: b'Not Found', | ||||
|     405: b'Method Not Allowed', | ||||
|     406: b'Not Acceptable', | ||||
|     407: b'Proxy Authentication Required', | ||||
|     408: b'Request Timeout', | ||||
|     409: b'Conflict', | ||||
|     410: b'Gone', | ||||
|     411: b'Length Required', | ||||
|     412: b'Precondition Failed', | ||||
|     413: b'Request Entity Too Large', | ||||
|     414: b'Request-URI Too Long', | ||||
|     415: b'Unsupported Media Type', | ||||
|     416: b'Requested Range Not Satisfiable', | ||||
|     417: b'Expectation Failed', | ||||
|     422: b'Unprocessable Entity', | ||||
|     423: b'Locked', | ||||
|     424: b'Failed Dependency', | ||||
|     426: b'Upgrade Required', | ||||
|     428: b'Precondition Required', | ||||
|     429: b'Too Many Requests', | ||||
|     431: b'Request Header Fields Too Large', | ||||
|     500: b'Internal Server Error', | ||||
|     501: b'Not Implemented', | ||||
|     502: b'Bad Gateway', | ||||
|     503: b'Service Unavailable', | ||||
|     504: b'Gateway Timeout', | ||||
|     505: b'HTTP Version Not Supported', | ||||
|     506: b'Variant Also Negotiates', | ||||
|     507: b'Insufficient Storage', | ||||
|     508: b'Loop Detected', | ||||
|     510: b'Not Extended', | ||||
|     511: b'Network Authentication Required' | ||||
| } | ||||
|  | ||||
|  | ||||
| class HTTPResponse: | ||||
|     __slots__ = ('body', 'status', 'content_type', 'headers') | ||||
|     __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') | ||||
|  | ||||
|     def __init__(self, body=None, status=200, headers=None, | ||||
|                  content_type='text/plain', body_bytes=b''): | ||||
| @@ -30,6 +85,7 @@ class HTTPResponse: | ||||
|  | ||||
|         self.status = status | ||||
|         self.headers = headers or {} | ||||
|         self._cookies = None | ||||
|  | ||||
|     def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||
|         # This is all returned in a kind-of funky way | ||||
| @@ -44,6 +100,13 @@ class HTTPResponse: | ||||
|                 b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) | ||||
|                 for name, value in self.headers.items() | ||||
|             ) | ||||
|  | ||||
|         # Try to pull from the common codes first | ||||
|         # Speeds up response rate 6% over pulling from all | ||||
|         status = COMMON_STATUS_CODES.get(self.status) | ||||
|         if not status: | ||||
|             status = ALL_STATUS_CODES.get(self.status) | ||||
|  | ||||
|         return (b'HTTP/%b %d %b\r\n' | ||||
|                 b'Content-Type: %b\r\n' | ||||
|                 b'Content-Length: %d\r\n' | ||||
| @@ -52,7 +115,7 @@ class HTTPResponse: | ||||
|                 b'%b') % ( | ||||
|             version.encode(), | ||||
|             self.status, | ||||
|             STATUS_CODES.get(self.status, b'FAIL'), | ||||
|             status, | ||||
|             self.content_type.encode(), | ||||
|             len(self.body), | ||||
|             b'keep-alive' if keep_alive else b'close', | ||||
| @@ -61,10 +124,16 @@ class HTTPResponse: | ||||
|             self.body | ||||
|         ) | ||||
|  | ||||
|     @property | ||||
|     def cookies(self): | ||||
|         if self._cookies is None: | ||||
|             self._cookies = CookieJar(self.headers) | ||||
|         return self._cookies | ||||
|  | ||||
|  | ||||
| def json(body, status=200, headers=None): | ||||
|     return HTTPResponse(ujson.dumps(body), headers=headers, status=status, | ||||
|                         content_type="application/json; charset=utf-8") | ||||
|     return HTTPResponse(json_dumps(body), headers=headers, status=status, | ||||
|                         content_type="application/json") | ||||
|  | ||||
|  | ||||
| def text(body, status=200, headers=None): | ||||
| @@ -75,3 +144,17 @@ def text(body, status=200, headers=None): | ||||
| def html(body, status=200, headers=None): | ||||
|     return HTTPResponse(body, status=status, headers=headers, | ||||
|                         content_type="text/html; charset=utf-8") | ||||
|  | ||||
|  | ||||
| async def file(location, mime_type=None, headers=None): | ||||
|     filename = path.split(location)[-1] | ||||
|  | ||||
|     async with open_async(location, mode='rb') as _file: | ||||
|         out_stream = await _file.read() | ||||
|  | ||||
|     mime_type = mime_type or guess_type(filename)[0] or 'text/plain' | ||||
|  | ||||
|     return HTTPResponse(status=200, | ||||
|                         headers=headers, | ||||
|                         content_type=mime_type, | ||||
|                         body_bytes=out_stream) | ||||
|   | ||||
							
								
								
									
										177
									
								
								sanic/router.py
									
									
									
									
									
								
							
							
						
						
									
										177
									
								
								sanic/router.py
									
									
									
									
									
								
							| @@ -1,9 +1,26 @@ | ||||
| import re | ||||
| from collections import namedtuple | ||||
| from collections import defaultdict, namedtuple | ||||
| from functools import lru_cache | ||||
| from .config import Config | ||||
| from .exceptions import NotFound, InvalidUsage | ||||
|  | ||||
| Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) | ||||
| Parameter = namedtuple("Parameter", ['name', 'cast']) | ||||
| Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) | ||||
| Parameter = namedtuple('Parameter', ['name', 'cast']) | ||||
|  | ||||
| REGEX_TYPES = { | ||||
|     'string': (str, r'[^/]+'), | ||||
|     'int': (int, r'\d+'), | ||||
|     'number': (float, r'[0-9\\.]+'), | ||||
|     'alpha': (str, r'[A-Za-z]+'), | ||||
| } | ||||
|  | ||||
|  | ||||
| def url_hash(url): | ||||
|     return url.count('/') | ||||
|  | ||||
|  | ||||
| class RouteExists(Exception): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| class Router: | ||||
| @@ -18,22 +35,16 @@ class Router: | ||||
|     function provided Parameters can also have a type by appending :type to | ||||
|     the <parameter>.  If no type is provided, a string is expected.  A regular | ||||
|     expression can also be passed in as the type | ||||
|  | ||||
|     TODO: | ||||
|         This probably needs optimization for larger sets of routes, | ||||
|         since it checks every route until it finds a match which is bad and | ||||
|         I should feel bad | ||||
|     """ | ||||
|     routes = None | ||||
|     regex_types = { | ||||
|         "string": (None, "[^/]+"), | ||||
|         "int": (int, "\d+"), | ||||
|         "number": (float, "[0-9\\.]+"), | ||||
|         "alpha": (None, "[A-Za-z]+"), | ||||
|     } | ||||
|     routes_static = None | ||||
|     routes_dynamic = None | ||||
|     routes_always_check = None | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.routes = [] | ||||
|         self.routes_all = {} | ||||
|         self.routes_static = {} | ||||
|         self.routes_dynamic = defaultdict(list) | ||||
|         self.routes_always_check = [] | ||||
|  | ||||
|     def add(self, uri, methods, handler): | ||||
|         """ | ||||
| @@ -45,42 +56,52 @@ class Router: | ||||
|         When executed, it should provide a response object. | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         if uri in self.routes_all: | ||||
|             raise RouteExists("Route already registered: {}".format(uri)) | ||||
|  | ||||
|         # Dict for faster lookups of if method allowed | ||||
|         methods_dict = None | ||||
|         if methods: | ||||
|             methods_dict = {method: True for method in methods} | ||||
|             methods = frozenset(methods) | ||||
|  | ||||
|         parameters = [] | ||||
|         properties = {"unhashable": None} | ||||
|  | ||||
|         def add_parameter(match): | ||||
|             # We could receive NAME or NAME:PATTERN | ||||
|             parts = match.group(1).split(':') | ||||
|             if len(parts) == 2: | ||||
|                 parameter_name, parameter_pattern = parts | ||||
|             else: | ||||
|                 parameter_name = parts[0] | ||||
|                 parameter_pattern = 'string' | ||||
|             name = match.group(1) | ||||
|             pattern = 'string' | ||||
|             if ':' in name: | ||||
|                 name, pattern = name.split(':', 1) | ||||
|  | ||||
|             default = (str, pattern) | ||||
|             # Pull from pre-configured types | ||||
|             parameter_regex = self.regex_types.get(parameter_pattern) | ||||
|             if parameter_regex: | ||||
|                 parameter_type, parameter_pattern = parameter_regex | ||||
|             else: | ||||
|                 parameter_type = None | ||||
|  | ||||
|             parameter = Parameter(name=parameter_name, cast=parameter_type) | ||||
|             _type, pattern = REGEX_TYPES.get(pattern, default) | ||||
|             parameter = Parameter(name=name, cast=_type) | ||||
|             parameters.append(parameter) | ||||
|  | ||||
|             return "({})".format(parameter_pattern) | ||||
|             # Mark the whole route as unhashable if it has the hash key in it | ||||
|             if re.search('(^|[^^]){1}/', pattern): | ||||
|                 properties['unhashable'] = True | ||||
|             # Mark the route as unhashable if it matches the hash key | ||||
|             elif re.search(pattern, '/'): | ||||
|                 properties['unhashable'] = True | ||||
|  | ||||
|         pattern_string = re.sub("<(.+?)>", add_parameter, uri) | ||||
|         pattern = re.compile("^{}$".format(pattern_string)) | ||||
|             return '({})'.format(pattern) | ||||
|  | ||||
|         pattern_string = re.sub(r'<(.+?)>', add_parameter, uri) | ||||
|         pattern = re.compile(r'^{}$'.format(pattern_string)) | ||||
|  | ||||
|         route = Route( | ||||
|             handler=handler, methods=methods_dict, pattern=pattern, | ||||
|             handler=handler, methods=methods, pattern=pattern, | ||||
|             parameters=parameters) | ||||
|         self.routes.append(route) | ||||
|  | ||||
|         self.routes_all[uri] = route | ||||
|         if properties['unhashable']: | ||||
|             self.routes_always_check.append(route) | ||||
|         elif parameters: | ||||
|             self.routes_dynamic[url_hash(uri)].append(route) | ||||
|         else: | ||||
|             self.routes_static[uri] = route | ||||
|  | ||||
|     def get(self, request): | ||||
|         """ | ||||
| @@ -89,58 +110,42 @@ class Router: | ||||
|         :param request: Request object | ||||
|         :return: handler, arguments, keyword arguments | ||||
|         """ | ||||
|         return self._get(request.url, request.method) | ||||
|  | ||||
|         route = None | ||||
|         args = [] | ||||
|         kwargs = {} | ||||
|         for _route in self.routes: | ||||
|             match = _route.pattern.match(request.url) | ||||
|     @lru_cache(maxsize=Config.ROUTER_CACHE_SIZE) | ||||
|     def _get(self, url, method): | ||||
|         """ | ||||
|         Gets a request handler based on the URL of the request, or raises an | ||||
|         error.  Internal method for caching. | ||||
|         :param url: Request URL | ||||
|         :param method: Request method | ||||
|         :return: handler, arguments, keyword arguments | ||||
|         """ | ||||
|         # Check against known static routes | ||||
|         route = self.routes_static.get(url) | ||||
|         if route: | ||||
|             match = route.pattern.match(url) | ||||
|         else: | ||||
|             # Move on to testing all regex routes | ||||
|             for route in self.routes_dynamic[url_hash(url)]: | ||||
|                 match = route.pattern.match(url) | ||||
|                 if match: | ||||
|                 for index, parameter in enumerate(_route.parameters, start=1): | ||||
|                     value = match.group(index) | ||||
|                     if parameter.cast: | ||||
|                         kwargs[parameter.name] = parameter.cast(value) | ||||
|                     else: | ||||
|                         kwargs[parameter.name] = value | ||||
|                 route = _route | ||||
|                     break | ||||
|  | ||||
|         if route: | ||||
|             if route.methods and request.method not in route.methods: | ||||
|                 raise InvalidUsage( | ||||
|                     "Method {} not allowed for URL {}".format( | ||||
|                         request.method, request.url), status_code=405) | ||||
|             return route.handler, args, kwargs | ||||
|             else: | ||||
|             raise NotFound("Requested URL {} not found".format(request.url)) | ||||
|  | ||||
|  | ||||
| class SimpleRouter: | ||||
|     """ | ||||
|     Simple router records and reads all routes from a dictionary | ||||
|     It does not support parameters in routes, but is very fast | ||||
|     """ | ||||
|     routes = None | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.routes = {} | ||||
|  | ||||
|     def add(self, uri, methods, handler): | ||||
|         # Dict for faster lookups of method allowed | ||||
|         methods_dict = None | ||||
|         if methods: | ||||
|             methods_dict = {method: True for method in methods} | ||||
|         self.routes[uri] = Route( | ||||
|             handler=handler, methods=methods_dict, pattern=uri, | ||||
|             parameters=None) | ||||
|  | ||||
|     def get(self, request): | ||||
|         route = self.routes.get(request.url) | ||||
|         if route: | ||||
|             if route.methods and request.method not in route.methods: | ||||
|                 raise InvalidUsage( | ||||
|                     "Method {} not allowed for URL {}".format( | ||||
|                         request.method, request.url), status_code=405) | ||||
|             return route.handler, [], {} | ||||
|                 # Lastly, check against all regex routes that cannot be hashed | ||||
|                 for route in self.routes_always_check: | ||||
|                     match = route.pattern.match(url) | ||||
|                     if match: | ||||
|                         break | ||||
|                 else: | ||||
|             raise NotFound("Requested URL {} not found".format(request.url)) | ||||
|                     raise NotFound('Requested URL {} not found'.format(url)) | ||||
|  | ||||
|         if route.methods and method not in route.methods: | ||||
|             raise InvalidUsage( | ||||
|                 'Method {} not allowed for URL {}'.format( | ||||
|                     method, url), status_code=405) | ||||
|  | ||||
|         kwargs = {p.name: p.cast(value) | ||||
|                   for value, p | ||||
|                   in zip(match.groups(1), route.parameters)} | ||||
|         return route.handler, [], kwargs | ||||
|   | ||||
							
								
								
									
										176
									
								
								sanic/sanic.py
									
									
									
									
									
								
							
							
						
						
									
										176
									
								
								sanic/sanic.py
									
									
									
									
									
								
							| @@ -1,5 +1,10 @@ | ||||
| import asyncio | ||||
| from asyncio import get_event_loop | ||||
| from collections import deque | ||||
| from functools import partial | ||||
| from inspect import isawaitable | ||||
| from multiprocessing import Process, Event | ||||
| from signal import signal, SIGTERM, SIGINT | ||||
| from time import sleep | ||||
| from traceback import format_exc | ||||
|  | ||||
| from .config import Config | ||||
| @@ -8,6 +13,7 @@ from .log import log, logging | ||||
| from .response import HTTPResponse | ||||
| from .router import Router | ||||
| from .server import serve | ||||
| from .static import register as static_register | ||||
| from .exceptions import ServerError | ||||
|  | ||||
|  | ||||
| @@ -17,10 +23,15 @@ class Sanic: | ||||
|         self.router = router or Router() | ||||
|         self.error_handler = error_handler or Handler(self) | ||||
|         self.config = Config() | ||||
|         self.request_middleware = [] | ||||
|         self.response_middleware = [] | ||||
|         self.request_middleware = deque() | ||||
|         self.response_middleware = deque() | ||||
|         self.blueprints = {} | ||||
|         self._blueprint_order = [] | ||||
|         self.loop = None | ||||
|         self.debug = None | ||||
|  | ||||
|         # Register alternative method names | ||||
|         self.go_fast = self.run | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Registration | ||||
| @@ -35,6 +46,11 @@ class Sanic: | ||||
|         :return: decorated function | ||||
|         """ | ||||
|  | ||||
|         # Fix case where the user did not prefix the URL with a / | ||||
|         # and will probably get confused as to why it's not working | ||||
|         if not uri.startswith('/'): | ||||
|             uri = '/' + uri | ||||
|  | ||||
|         def response(handler): | ||||
|             self.router.add(uri=uri, methods=methods, handler=handler) | ||||
|             return handler | ||||
| @@ -44,9 +60,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 | ||||
|         """ | ||||
|  | ||||
| @@ -69,7 +84,7 @@ class Sanic: | ||||
|             if attach_to == 'request': | ||||
|                 self.request_middleware.append(middleware) | ||||
|             if attach_to == 'response': | ||||
|                 self.response_middleware.append(middleware) | ||||
|                 self.response_middleware.appendleft(middleware) | ||||
|             return middleware | ||||
|  | ||||
|         # Detect which way this was called, @middleware or @middleware('AT') | ||||
| @@ -79,7 +94,17 @@ class Sanic: | ||||
|             attach_to = args[0] | ||||
|             return register_middleware | ||||
|  | ||||
|     def register_blueprint(self, blueprint, **options): | ||||
|     # Static Files | ||||
|     def static(self, uri, file_or_directory, pattern='.+', | ||||
|                use_modified_since=True): | ||||
|         """ | ||||
|         Registers a root to serve files from.  The input can either be a file | ||||
|         or a directory.  See | ||||
|         """ | ||||
|         static_register(self, uri, file_or_directory, pattern, | ||||
|                         use_modified_since) | ||||
|  | ||||
|     def blueprint(self, blueprint, **options): | ||||
|         """ | ||||
|         Registers a blueprint on the application. | ||||
|         :param blueprint: Blueprint object | ||||
| @@ -96,10 +121,19 @@ class Sanic: | ||||
|             self._blueprint_order.append(blueprint) | ||||
|         blueprint.register(self, options) | ||||
|  | ||||
|     def register_blueprint(self, *args, **kwargs): | ||||
|         # TODO: deprecate 1.0 | ||||
|         log.warning("Use of register_blueprint will be deprecated in " | ||||
|                     "version 1.0.  Please use the blueprint method instead") | ||||
|         return self.blueprint(*args, **kwargs) | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Request Handling | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     def converted_response_type(self, response): | ||||
|         pass | ||||
|  | ||||
|     async def handle_request(self, request, response_callback): | ||||
|         """ | ||||
|         Takes a request from the HTTP Server and returns a response object to | ||||
| @@ -111,7 +145,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: | ||||
| @@ -124,6 +161,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: | ||||
| @@ -136,7 +177,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) | ||||
| @@ -147,6 +191,10 @@ class Sanic: | ||||
|                             break | ||||
|  | ||||
|         except Exception as e: | ||||
|             # -------------------------------------------- # | ||||
|             # Response Generation Failed | ||||
|             # -------------------------------------------- # | ||||
|  | ||||
|             try: | ||||
|                 response = self.error_handler.response(request, e) | ||||
|                 if isawaitable(response): | ||||
| @@ -166,22 +214,66 @@ class Sanic: | ||||
|     # Execution | ||||
|     # -------------------------------------------------------------------- # | ||||
|  | ||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, | ||||
|             before_stop=None): | ||||
|     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, | ||||
|             'port': port, | ||||
|             'sock': sock, | ||||
|             'debug': debug, | ||||
|             '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) | ||||
| @@ -191,23 +283,59 @@ class Sanic: | ||||
|         log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) | ||||
|  | ||||
|         try: | ||||
|             serve( | ||||
|                 host=host, | ||||
|                 port=port, | ||||
|                 debug=debug, | ||||
|                 after_start=after_start, | ||||
|                 before_stop=before_stop, | ||||
|                 request_handler=self.handle_request, | ||||
|                 request_timeout=self.config.REQUEST_TIMEOUT, | ||||
|                 request_max_size=self.config.REQUEST_MAX_SIZE, | ||||
|             ) | ||||
|             if workers == 1: | ||||
|                 serve(**server_settings) | ||||
|             else: | ||||
|                 log.info('Spinning up {} workers...'.format(workers)) | ||||
|  | ||||
|                 self.serve_multiple(server_settings, workers) | ||||
|  | ||||
|         except Exception as e: | ||||
|             log.exception( | ||||
|                 'Experienced exception while trying to serve: {}'.format(e)) | ||||
|             pass | ||||
|  | ||||
|         log.info("Server Stopped") | ||||
|  | ||||
|     def stop(self): | ||||
|         """ | ||||
|         This kills the Sanic | ||||
|         """ | ||||
|         asyncio.get_event_loop().stop() | ||||
|         get_event_loop().stop() | ||||
|  | ||||
|     @staticmethod | ||||
|     def serve_multiple(server_settings, workers, stop_event=None): | ||||
|         """ | ||||
|         Starts multiple server processes simultaneously.  Stops on interrupt | ||||
|         and terminate signals, and drains connections when complete. | ||||
|         :param server_settings: kw arguments to be passed to the serve function | ||||
|         :param workers: number of workers to launch | ||||
|         :param stop_event: if provided, is used as a stop signal | ||||
|         :return: | ||||
|         """ | ||||
|         server_settings['reuse_port'] = True | ||||
|  | ||||
|         # Create a stop event to be triggered by a signal | ||||
|         if not stop_event: | ||||
|             stop_event = Event() | ||||
|         signal(SIGINT, lambda s, f: stop_event.set()) | ||||
|         signal(SIGTERM, lambda s, f: stop_event.set()) | ||||
|  | ||||
|         processes = [] | ||||
|         for _ in range(workers): | ||||
|             process = Process(target=serve, kwargs=server_settings) | ||||
|             process.start() | ||||
|             processes.append(process) | ||||
|  | ||||
|         # Infinitely wait for the stop event | ||||
|         try: | ||||
|             while not stop_event.is_set(): | ||||
|                 sleep(0.3) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|         log.info('Spinning down workers...') | ||||
|         for process in processes: | ||||
|             process.terminate() | ||||
|         for process in processes: | ||||
|             process.join() | ||||
|   | ||||
| @@ -110,6 +110,9 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         ) | ||||
|  | ||||
|     def on_body(self, body): | ||||
|         if self.request.body: | ||||
|             self.request.body += body | ||||
|         else: | ||||
|             self.request.body = body | ||||
|  | ||||
|     def on_message_complete(self): | ||||
| @@ -122,8 +125,8 @@ class HttpProtocol(asyncio.Protocol): | ||||
|  | ||||
|     def write_response(self, response): | ||||
|         try: | ||||
|             keep_alive = all( | ||||
|                 [self.parser.should_keep_alive(), self.signal.stopped]) | ||||
|             keep_alive = self.parser.should_keep_alive() \ | ||||
|                             and not self.signal.stopped | ||||
|             self.transport.write( | ||||
|                 response.output( | ||||
|                     self.request.version, keep_alive, self.request_timeout)) | ||||
| @@ -157,15 +160,48 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         return False | ||||
|  | ||||
|  | ||||
| def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
|           debug=False, request_timeout=60, | ||||
|           request_max_size=None): | ||||
|     # Create Event Loop | ||||
|     loop = async_loop.new_event_loop() | ||||
| def trigger_events(events, loop): | ||||
|     """ | ||||
|     :param events: one or more sync or async functions to execute | ||||
|     :param loop: event loop | ||||
|     """ | ||||
|     if events: | ||||
|         if not isinstance(events, list): | ||||
|             events = [events] | ||||
|         for event in events: | ||||
|             result = event(loop) | ||||
|             if isawaitable(result): | ||||
|                 loop.run_until_complete(result) | ||||
|  | ||||
|  | ||||
| def serve(host, port, request_handler, before_start=None, after_start=None, | ||||
|           before_stop=None, after_stop=None, | ||||
|           debug=False, request_timeout=60, sock=None, | ||||
|           request_max_size=None, reuse_port=False, loop=None): | ||||
|     """ | ||||
|     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() | ||||
| @@ -176,18 +212,15 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, | ||||
|         request_handler=request_handler, | ||||
|         request_timeout=request_timeout, | ||||
|         request_max_size=request_max_size, | ||||
|     ), host, port) | ||||
|     ), 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)) | ||||
|     except Exception: | ||||
|         log.exception("Unable to start server") | ||||
|         return | ||||
|  | ||||
|     # Run the on_start function if provided | ||||
|     if after_start: | ||||
|         result = after_start(loop) | ||||
|         if isawaitable(result): | ||||
|             loop.run_until_complete(result) | ||||
|     trigger_events(after_start, loop) | ||||
|  | ||||
|     # Register signals for graceful termination | ||||
|     for _signal in (SIGINT, SIGTERM): | ||||
| @@ -199,10 +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,5 +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() | ||||
|         log.info("Server Stopped") | ||||
|   | ||||
							
								
								
									
										59
									
								
								sanic/static.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								sanic/static.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| from aiofiles.os import stat | ||||
| from os import path | ||||
| from re import sub | ||||
| from time import strftime, gmtime | ||||
|  | ||||
| from .exceptions import FileNotFound, InvalidUsage | ||||
| from .response import file, HTTPResponse | ||||
|  | ||||
|  | ||||
| def register(app, uri, file_or_directory, pattern, use_modified_since): | ||||
|     # TODO: Though sanic is not a file server, I feel like we should atleast | ||||
|     #       make a good effort here.  Modified-since is nice, but we could | ||||
|     #       also look into etags, expires, and caching | ||||
|     """ | ||||
|     Registers a static directory handler with Sanic by adding a route to the | ||||
|     router and registering a handler. | ||||
|     :param app: Sanic | ||||
|     :param file_or_directory: File or directory path to serve from | ||||
|     :param uri: URL to serve from | ||||
|     :param pattern: regular expression used to match files in the URL | ||||
|     :param use_modified_since: If true, send file modified time, and return | ||||
|                      not modified if the browser's matches the server's | ||||
|     """ | ||||
|  | ||||
|     # If we're not trying to match a file directly, | ||||
|     # serve from the folder | ||||
|     if not path.isfile(file_or_directory): | ||||
|         uri += '<file_uri:' + pattern + '>' | ||||
|  | ||||
|     async def _handler(request, file_uri=None): | ||||
|         # Using this to determine if the URL is trying to break out of the path | ||||
|         # served.  os.path.realpath seems to be very slow | ||||
|         if file_uri and '../' in file_uri: | ||||
|             raise InvalidUsage("Invalid URL") | ||||
|  | ||||
|         # Merge served directory and requested file if provided | ||||
|         # Strip all / that in the beginning of the URL to help prevent python | ||||
|         # from herping a derp and treating the uri as an absolute path | ||||
|         file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ | ||||
|             if file_uri else file_or_directory | ||||
|         try: | ||||
|             headers = {} | ||||
|             # Check if the client has been sent this file before | ||||
|             # and it has not been modified since | ||||
|             if use_modified_since: | ||||
|                 stats = await stat(file_path) | ||||
|                 modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT', | ||||
|                                           gmtime(stats.st_mtime)) | ||||
|                 if request.headers.get('If-Modified-Since') == modified_since: | ||||
|                     return HTTPResponse(status=304) | ||||
|                 headers['Last-Modified'] = modified_since | ||||
|  | ||||
|             return await file(file_path, headers=headers) | ||||
|         except: | ||||
|             raise FileNotFound('File not found', | ||||
|                                path=file_or_directory, | ||||
|                                relative_url=file_uri) | ||||
|  | ||||
|     app.route(uri, methods=['GET'])(_handler) | ||||
| @@ -5,12 +5,13 @@ HOST = '127.0.0.1' | ||||
| PORT = 42101 | ||||
|  | ||||
|  | ||||
| async def local_request(method, uri, *args, **kwargs): | ||||
| async def local_request(method, uri, cookies=None, *args, **kwargs): | ||||
|     url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) | ||||
|     log.info(url) | ||||
|     async with aiohttp.ClientSession() as session: | ||||
|     async with aiohttp.ClientSession(cookies=cookies) as session: | ||||
|         async with getattr(session, method)(url, *args, **kwargs) as response: | ||||
|             response.text = await response.text() | ||||
|             response.body = await response.read() | ||||
|             return response | ||||
|  | ||||
|  | ||||
| @@ -24,7 +25,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) | ||||
|   | ||||
							
								
								
									
										15
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								setup.py
									
									
									
									
									
								
							| @@ -1,11 +1,23 @@ | ||||
| """ | ||||
| Sanic | ||||
| """ | ||||
| import codecs | ||||
| import os | ||||
| import re | ||||
| from setuptools import setup | ||||
|  | ||||
|  | ||||
| with codecs.open(os.path.join(os.path.abspath(os.path.dirname( | ||||
|         __file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp: | ||||
|     try: | ||||
|         version = re.findall(r"^__version__ = '([^']+)'\r?$", | ||||
|                              fp.read(), re.M)[0] | ||||
|     except IndexError: | ||||
|         raise RuntimeError('Unable to determine version.') | ||||
|  | ||||
| setup( | ||||
|     name='Sanic', | ||||
|     version="0.1.3", | ||||
|     version=version, | ||||
|     url='http://github.com/channelcat/sanic/', | ||||
|     license='MIT', | ||||
|     author='Channel Cat', | ||||
| @@ -17,6 +29,7 @@ setup( | ||||
|         'uvloop>=0.5.3', | ||||
|         'httptools>=0.0.9', | ||||
|         'ujson>=1.35', | ||||
|         'aiofiles>=0.3.0', | ||||
|     ], | ||||
|     classifiers=[ | ||||
|         'Development Status :: 2 - Pre-Alpha', | ||||
|   | ||||
							
								
								
									
										11
									
								
								tests/performance/falcon/simple_server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								tests/performance/falcon/simple_server.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| # Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker falc:app | ||||
|  | ||||
| import falcon | ||||
| import ujson as json | ||||
|  | ||||
| class TestResource: | ||||
|     def on_get(self, req, resp): | ||||
|         resp.body = json.dumps({"test": True}) | ||||
|  | ||||
| app = falcon.API() | ||||
| app.add_route('/', TestResource()) | ||||
| @@ -15,5 +15,5 @@ app = Sanic("test") | ||||
| async def test(request): | ||||
|     return json({"test": True}) | ||||
|  | ||||
|  | ||||
| app.run(host="0.0.0.0", port=sys.argv[1]) | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="0.0.0.0", port=sys.argv[1]) | ||||
|   | ||||
							
								
								
									
										19
									
								
								tests/performance/tornado/simple_server.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								tests/performance/tornado/simple_server.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| # Run with: python simple_server.py | ||||
| import ujson | ||||
| from tornado import ioloop, web | ||||
|  | ||||
|  | ||||
| class MainHandler(web.RequestHandler): | ||||
|     def get(self): | ||||
|         self.write(ujson.dumps({'test': True})) | ||||
|  | ||||
|  | ||||
| app = web.Application([ | ||||
|     (r'/', MainHandler) | ||||
| ],  debug=False, | ||||
|     compress_response=False, | ||||
|     static_hash_cache=True | ||||
| ) | ||||
|  | ||||
| app.listen(8000) | ||||
| ioloop.IOLoop.current().start() | ||||
| @@ -1,3 +1,5 @@ | ||||
| import inspect | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
| from sanic.response import json, text | ||||
| @@ -17,7 +19,7 @@ def test_bp(): | ||||
|     def handler(request): | ||||
|         return text('Hello') | ||||
|  | ||||
|     app.register_blueprint(bp) | ||||
|     app.blueprint(bp) | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|  | ||||
|     assert response.text == 'Hello' | ||||
| @@ -30,7 +32,7 @@ def test_bp_with_url_prefix(): | ||||
|     def handler(request): | ||||
|         return text('Hello') | ||||
|  | ||||
|     app.register_blueprint(bp) | ||||
|     app.blueprint(bp) | ||||
|     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||
|  | ||||
|     assert response.text == 'Hello' | ||||
| @@ -49,8 +51,8 @@ def test_several_bp_with_url_prefix(): | ||||
|     def handler2(request): | ||||
|         return text('Hello2') | ||||
|  | ||||
|     app.register_blueprint(bp) | ||||
|     app.register_blueprint(bp2) | ||||
|     app.blueprint(bp) | ||||
|     app.blueprint(bp2) | ||||
|     request, response = sanic_endpoint_test(app, uri='/test1/') | ||||
|     assert response.text == 'Hello' | ||||
|  | ||||
| @@ -70,7 +72,7 @@ def test_bp_middleware(): | ||||
|     async def handler(request): | ||||
|         return text('FAIL') | ||||
|  | ||||
|     app.register_blueprint(blueprint) | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|  | ||||
| @@ -97,7 +99,7 @@ def test_bp_exception_handler(): | ||||
|     def handler_exception(request, exception): | ||||
|         return text("OK") | ||||
|  | ||||
|     app.register_blueprint(blueprint) | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/1') | ||||
|     assert response.status == 400 | ||||
| @@ -109,3 +111,55 @@ def test_bp_exception_handler(): | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/3') | ||||
|     assert response.status == 200 | ||||
|  | ||||
| def test_bp_listeners(): | ||||
|     app = Sanic('test_middleware') | ||||
|     blueprint = Blueprint('test_middleware') | ||||
|  | ||||
|     order = [] | ||||
|  | ||||
|     @blueprint.listener('before_server_start') | ||||
|     def handler_1(sanic, loop): | ||||
|         order.append(1) | ||||
|  | ||||
|     @blueprint.listener('after_server_start') | ||||
|     def handler_2(sanic, loop): | ||||
|         order.append(2) | ||||
|  | ||||
|     @blueprint.listener('after_server_start') | ||||
|     def handler_3(sanic, loop): | ||||
|         order.append(3) | ||||
|  | ||||
|     @blueprint.listener('before_server_stop') | ||||
|     def handler_4(sanic, loop): | ||||
|         order.append(5) | ||||
|  | ||||
|     @blueprint.listener('before_server_stop') | ||||
|     def handler_5(sanic, loop): | ||||
|         order.append(4) | ||||
|  | ||||
|     @blueprint.listener('after_server_stop') | ||||
|     def handler_6(sanic, loop): | ||||
|         order.append(6) | ||||
|  | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/') | ||||
|  | ||||
|     assert order == [1,2,3,4,5,6] | ||||
|  | ||||
| def test_bp_static(): | ||||
|     current_file = inspect.getfile(inspect.currentframe()) | ||||
|     with open(current_file, 'rb') as file: | ||||
|         current_file_contents = file.read() | ||||
|  | ||||
|     app = Sanic('test_static') | ||||
|     blueprint = Blueprint('test_static') | ||||
|  | ||||
|     blueprint.static('/testing.file', current_file) | ||||
|  | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/testing.file') | ||||
|     assert response.status == 200 | ||||
|     assert response.body == current_file_contents | ||||
							
								
								
									
										44
									
								
								tests/test_cookies.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								tests/test_cookies.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | ||||
| from datetime import datetime, timedelta | ||||
| from http.cookies import SimpleCookie | ||||
| from sanic import Sanic | ||||
| from sanic.response import json, text | ||||
| from sanic.utils import sanic_endpoint_test | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  GET | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| def test_cookies(): | ||||
|     app = Sanic('test_text') | ||||
|  | ||||
|     @app.route('/') | ||||
|     def handler(request): | ||||
|         response = text('Cookies are: {}'.format(request.cookies['test'])) | ||||
|         response.cookies['right_back'] = 'at you' | ||||
|         return response | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, cookies={"test": "working!"}) | ||||
|     response_cookies = SimpleCookie() | ||||
|     response_cookies.load(response.headers.get('Set-Cookie', {})) | ||||
|  | ||||
|     assert response.text == 'Cookies are: working!' | ||||
|     assert response_cookies['right_back'].value == 'at you' | ||||
|  | ||||
| def test_cookie_options(): | ||||
|     app = Sanic('test_text') | ||||
|  | ||||
|     @app.route('/') | ||||
|     def handler(request): | ||||
|         response = text("OK") | ||||
|         response.cookies['test'] = 'at you' | ||||
|         response.cookies['test']['httponly'] = True | ||||
|         response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10) | ||||
|         return response | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|     response_cookies = SimpleCookie() | ||||
|     response_cookies.load(response.headers.get('Set-Cookie', {})) | ||||
|  | ||||
|     assert response_cookies['test'].value == 'at you' | ||||
|     assert response_cookies['test']['httponly'] == True | ||||
| @@ -86,3 +86,43 @@ def test_middleware_override_response(): | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|  | ||||
|  | ||||
| def test_middleware_order(): | ||||
|     app = Sanic('test_middleware_order') | ||||
|  | ||||
|     order = [] | ||||
|  | ||||
|     @app.middleware('request') | ||||
|     async def request1(request): | ||||
|         order.append(1) | ||||
|  | ||||
|     @app.middleware('request') | ||||
|     async def request2(request): | ||||
|         order.append(2) | ||||
|  | ||||
|     @app.middleware('request') | ||||
|     async def request3(request): | ||||
|         order.append(3) | ||||
|  | ||||
|     @app.middleware('response') | ||||
|     async def response1(request, response): | ||||
|         order.append(6) | ||||
|  | ||||
|     @app.middleware('response') | ||||
|     async def response2(request, response): | ||||
|         order.append(5) | ||||
|  | ||||
|     @app.middleware('response') | ||||
|     async def response3(request, response): | ||||
|         order.append(4) | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app) | ||||
|  | ||||
|     assert response.status == 200 | ||||
|     assert order == [1,2,3,4,5,6] | ||||
|   | ||||
							
								
								
									
										53
									
								
								tests/test_multiprocessing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								tests/test_multiprocessing.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| 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 | ||||
| # ------------------------------------------------------------ # | ||||
|  | ||||
| # TODO: Figure out why this freezes on pytest but not when | ||||
| # executed via interpreter | ||||
|  | ||||
| def skip_test_multiprocessing(): | ||||
|     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 | ||||
| @@ -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,59 @@ 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 | ||||
|  | ||||
|  | ||||
| def test_method_not_allowed(): | ||||
|     app = Sanic('test_method_not_allowed') | ||||
|  | ||||
|     @app.route('/test', methods=['GET']) | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/test') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, method='post', uri='/test') | ||||
|     assert response.status == 405 | ||||
|   | ||||
							
								
								
									
										30
									
								
								tests/test_static.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								tests/test_static.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import inspect | ||||
| import os | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.utils import sanic_endpoint_test | ||||
|  | ||||
| def test_static_file(): | ||||
|     current_file = inspect.getfile(inspect.currentframe()) | ||||
|     with open(current_file, 'rb') as file: | ||||
|         current_file_contents = file.read() | ||||
|  | ||||
|     app = Sanic('test_static') | ||||
|     app.static('/testing.file', current_file) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/testing.file') | ||||
|     assert response.status == 200 | ||||
|     assert response.body == current_file_contents | ||||
|  | ||||
| def test_static_directory(): | ||||
|     current_file = inspect.getfile(inspect.currentframe()) | ||||
|     current_directory = os.path.dirname(os.path.abspath(current_file)) | ||||
|     with open(current_file, 'rb') as file: | ||||
|         current_file_contents = file.read() | ||||
|  | ||||
|     app = Sanic('test_static') | ||||
|     app.static('/dir', current_directory) | ||||
|  | ||||
|     request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') | ||||
|     assert response.status == 200 | ||||
|     assert response.body == current_file_contents | ||||
		Reference in New Issue
	
	Block a user
	 Abhishek
					Abhishek