Compare commits
	
		
			30 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1036242064 | ||
|   | 5fd62098bd | ||
|   | b3814ca89a | ||
|   | b75a321e4a | ||
|   | 9caa4fec4a | ||
|   | a0cba1aee1 | ||
|   | 97018ad62f | ||
|   | a7d17fae44 | ||
|   | 6ce0050979 | ||
|   | bc035fca78 | ||
|   | df914a92e4 | ||
|   | 1b939a6823 | ||
|   | 81b6d988ec | ||
|   | 7e9b65feca | ||
|   | 6f098b3d21 | ||
|   | 5ddb0488f2 | ||
|   | 3e87314adf | ||
|   | f6d4a06661 | ||
|   | ff17fc95e6 | ||
|   | c5a46f1cea | ||
|   | 0b072189c4 | ||
|   | 5b22d1486a | ||
|   | 9eb48c2b0d | ||
|   | ff0632001c | ||
|   | 28bd09a2ea | ||
|   | c6aaa9b09c | ||
|   | 20d9ec1fd2 | ||
|   | 2c45c2d3c0 | ||
|   | 4c66cb1854 | ||
|   | 35b92e1511 | 
							
								
								
									
										62
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| # Contributing | ||||
|  | ||||
| Thank you for your interest! Sanic is always looking for contributors. If you | ||||
| don't feel comfortable contributing code, adding docstrings to the source files | ||||
| is very appreciated. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| To develop on sanic (and mainly to just run the tests) it is highly recommend to | ||||
| install from sources. | ||||
|  | ||||
| So assume you have already cloned the repo and are in the working directory with | ||||
| a virtual environment already set up, then run: | ||||
|  | ||||
| ```bash | ||||
| python setup.py develop && pip install -r requirements-dev.txt | ||||
| ``` | ||||
|  | ||||
| ## Running tests | ||||
|  | ||||
| To run the tests for sanic it is recommended to use tox like so: | ||||
|  | ||||
| ```bash | ||||
| tox | ||||
| ``` | ||||
|  | ||||
| See it's that simple! | ||||
|  | ||||
| ## Pull requests! | ||||
|  | ||||
| So the pull request approval rules are pretty simple: | ||||
| 1. All pull requests must pass unit tests | ||||
| 2. All pull requests must be reviewed and approved by at least  | ||||
| one current collaborator on the project | ||||
| 3. All pull requests must pass flake8 checks | ||||
| 4. If you decide to remove/change anything from any common interface | ||||
| a deprecation message should accompany it. | ||||
| 5. If you implement a new feature you should have at least one unit | ||||
| test to accompany it. | ||||
|  | ||||
| ## Documentation | ||||
|  | ||||
| Sanic's documentation is built | ||||
| using [sphinx](http://www.sphinx-doc.org/en/1.5.1/). Guides are written in | ||||
| Markdown and can be found in the `docs` folder, while the module reference is | ||||
| automatically generated using `sphinx-apidoc`. | ||||
|  | ||||
| To generate the documentation from scratch: | ||||
|  | ||||
| ```bash | ||||
| sphinx-apidoc -fo docs/_api/ sanic | ||||
| sphinx-build -b html docs docs/_build | ||||
| ``` | ||||
|  | ||||
| The HTML documentation will be created in the `docs/_build` folder. | ||||
|  | ||||
| ## Warning | ||||
|  | ||||
| One of the main goals of Sanic is speed. Code that lowers the performance of | ||||
| Sanic without significant gains in usability, security, or features may not be | ||||
| merged. Please don't let this intimidate you! If you have any concerns about an | ||||
| idea, open an issue for discussion and help. | ||||
| @@ -83,3 +83,4 @@ Out of the box there are just a few predefined values which can be overwritten w | ||||
|     | ----------------- | --------- | --------------------------------- | | ||||
|     | REQUEST_MAX_SIZE  | 100000000 | How big a request may be (bytes)  | | ||||
|     | REQUEST_TIMEOUT   | 60        | How long a request can take (sec) | | ||||
|     | KEEP_ALIVE        | True      | Disables keep-alive when False    | | ||||
|   | ||||
| @@ -4,10 +4,39 @@ Thank you for your interest! Sanic is always looking for contributors. If you | ||||
| don't feel comfortable contributing code, adding docstrings to the source files | ||||
| is very appreciated. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| To develop on sanic (and mainly to just run the tests) it is highly recommend to | ||||
| install from sources. | ||||
|  | ||||
| So assume you have already cloned the repo and are in the working directory with | ||||
| a virtual environment already set up, then run: | ||||
|  | ||||
| ```bash | ||||
| python setup.py develop && pip install -r requirements-dev.txt | ||||
| ``` | ||||
|  | ||||
| ## Running tests | ||||
|  | ||||
| * `python -m pip install pytest` | ||||
| * `python -m pytest tests` | ||||
| To run the tests for sanic it is recommended to use tox like so: | ||||
|  | ||||
| ```bash | ||||
| tox | ||||
| ``` | ||||
|  | ||||
| See it's that simple! | ||||
|  | ||||
| ## Pull requests! | ||||
|  | ||||
| So the pull request approval rules are pretty simple: | ||||
| 1. All pull requests must pass unit tests | ||||
| * All pull requests must be reviewed and approved by at least  | ||||
| one current collaborator on the project | ||||
| * All pull requests must pass flake8 checks | ||||
| * If you decide to remove/change anything from any common interface | ||||
| a deprecation message should accompany it. | ||||
| * If you implement a new feature you should have at least one unit | ||||
| test to accompany it. | ||||
|  | ||||
| ## Documentation | ||||
|  | ||||
|   | ||||
							
								
								
									
										41
									
								
								examples/dask_distributed.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								examples/dask_distributed.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
|  | ||||
| from tornado.platform.asyncio import BaseAsyncIOLoop, to_asyncio_future | ||||
| from distributed import LocalCluster, Client | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
|  | ||||
| def square(x): | ||||
|     return x**2 | ||||
|  | ||||
|  | ||||
| @app.listener('after_server_start') | ||||
| async def setup(app, loop): | ||||
|     # configure tornado use asyncio's loop | ||||
|     ioloop = BaseAsyncIOLoop(loop) | ||||
|  | ||||
|     # init distributed client | ||||
|     app.client = Client('tcp://localhost:8786', loop=ioloop, start=False) | ||||
|     await to_asyncio_future(app.client._start()) | ||||
|  | ||||
|  | ||||
| @app.listener('before_server_stop') | ||||
| async def stop(app, loop): | ||||
|     await to_asyncio_future(app.client._shutdown()) | ||||
|  | ||||
|  | ||||
| @app.route('/<value:int>') | ||||
| async def test(request, value): | ||||
|     future = app.client.submit(square, value) | ||||
|     result = await to_asyncio_future(future._result()) | ||||
|     return response.text(f'The square of {value} is {result}') | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     # Distributed cluster should run somewhere else | ||||
|     with LocalCluster(scheduler_port=8786, nanny=False, n_workers=2, | ||||
|                       threads_per_worker=1) as cluster: | ||||
|         app.run(host="0.0.0.0", port=8000) | ||||
| @@ -1,18 +1,27 @@ | ||||
| ## To use this example: | ||||
| # curl -d '{"name": "John Doe"}' localhost:8000 | ||||
| # Render templates in a Flask like way from a "template" directory in the project | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
| from jinja2 import Template | ||||
|  | ||||
| template = Template('Hello {{ name }}!') | ||||
| from jinja2 import Evironment, PackageLoader, select_autoescape | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| # Load the template environment with async support | ||||
| template_env = Environment( | ||||
|     loader=jinja2.PackageLoader('yourapplication', 'templates'), | ||||
|     autoescape=jinja2.select_autoescape(['html', 'xml']), | ||||
|     enable_async=True | ||||
| ) | ||||
|  | ||||
| # Load the template from file | ||||
| template = template_env.get_template("example_template.html") | ||||
|  | ||||
|  | ||||
| @app.route('/') | ||||
| async def test(request): | ||||
|     data = request.json | ||||
|     return response.html(template.render(**data)) | ||||
|     rendered_template = await template.render_async(**data) | ||||
|     return response.html(rendered_template) | ||||
|  | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8080, debug=True) | ||||
| @@ -1,6 +1,6 @@ | ||||
| from sanic.app import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
|  | ||||
| __version__ = '0.5.1' | ||||
| __version__ = '0.5.2' | ||||
|  | ||||
| __all__ = ['Sanic', 'Blueprint'] | ||||
|   | ||||
| @@ -16,7 +16,7 @@ from sanic.handlers import ErrorHandler | ||||
| from sanic.log import log | ||||
| from sanic.response import HTTPResponse, StreamingHTTPResponse | ||||
| from sanic.router import Router | ||||
| from sanic.server import serve, serve_multiple, HttpProtocol | ||||
| from sanic.server import serve, serve_multiple, HttpProtocol, Signal | ||||
| from sanic.static import register as static_register | ||||
| from sanic.testing import SanicTestClient | ||||
| from sanic.views import CompositionView | ||||
| @@ -288,7 +288,7 @@ class Sanic: | ||||
|                            attach_to=middleware_or_request) | ||||
|  | ||||
|     # Static Files | ||||
|     def static(self, uri, file_or_directory, pattern='.+', | ||||
|     def static(self, uri, file_or_directory, pattern=r'/?.+', | ||||
|                use_modified_since=True, use_content_range=False): | ||||
|         """Register a root to serve files from. The input can either be a | ||||
|         file or a directory. See | ||||
| @@ -674,11 +674,13 @@ class Sanic: | ||||
|             'port': port, | ||||
|             'sock': sock, | ||||
|             'ssl': ssl, | ||||
|             'signal': Signal(), | ||||
|             'debug': debug, | ||||
|             'request_handler': self.handle_request, | ||||
|             'error_handler': self.error_handler, | ||||
|             'request_timeout': self.config.REQUEST_TIMEOUT, | ||||
|             'request_max_size': self.config.REQUEST_MAX_SIZE, | ||||
|             'keep_alive': self.config.KEEP_ALIVE, | ||||
|             'loop': loop, | ||||
|             'register_sys_signals': register_sys_signals, | ||||
|             'backlog': backlog | ||||
|   | ||||
| @@ -6,7 +6,7 @@ SANIC_PREFIX = 'SANIC_' | ||||
|  | ||||
|  | ||||
| class Config(dict): | ||||
|     def __init__(self, defaults=None, load_env=True): | ||||
|     def __init__(self, defaults=None, load_env=True, keep_alive=True): | ||||
|         super().__init__(defaults or {}) | ||||
|         self.LOGO = """ | ||||
|                  ▄▄▄▄▄ | ||||
| @@ -31,6 +31,7 @@ class Config(dict): | ||||
| """ | ||||
|         self.REQUEST_MAX_SIZE = 100000000  # 100 megababies | ||||
|         self.REQUEST_TIMEOUT = 60  # 60 seconds | ||||
|         self.KEEP_ALIVE = keep_alive | ||||
|  | ||||
|         if load_env: | ||||
|             self.load_environment_vars() | ||||
| @@ -98,11 +99,11 @@ class Config(dict): | ||||
|                 self[key] = getattr(obj, key) | ||||
|  | ||||
|     def load_environment_vars(self): | ||||
|         """ | ||||
|         Looks for any SANIC_ prefixed environment variables and applies | ||||
|         them to the configuration if present. | ||||
|         """ | ||||
|         for k, v in os.environ.items(): | ||||
|             """ | ||||
|             Looks for any SANIC_ prefixed environment variables and applies | ||||
|             them to the configuration if present. | ||||
|             """ | ||||
|             if k.startswith(SANIC_PREFIX): | ||||
|                 _, config_key = k.split(SANIC_PREFIX, 1) | ||||
|                 self[config_key] = v | ||||
|   | ||||
| @@ -78,9 +78,10 @@ class Request(dict): | ||||
|         :return: token related to request | ||||
|         """ | ||||
|         auth_header = self.headers.get('Authorization') | ||||
|         if auth_header is not None: | ||||
|             return auth_header.split()[1] | ||||
|         return auth_header | ||||
|         if 'Token ' in auth_header: | ||||
|             return auth_header.partition('Token ')[-1] | ||||
|         else: | ||||
|             return auth_header | ||||
|  | ||||
|     @property | ||||
|     def form(self): | ||||
|   | ||||
| @@ -210,7 +210,7 @@ class HTTPResponse(BaseHTTPResponse): | ||||
|         # 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) | ||||
|             status = ALL_STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE') | ||||
|  | ||||
|         return (b'HTTP/%b %d %b\r\n' | ||||
|                 b'Connection: %b\r\n' | ||||
|   | ||||
| @@ -16,6 +16,7 @@ REGEX_TYPES = { | ||||
|     'int': (int, r'\d+'), | ||||
|     'number': (float, r'[0-9\\.]+'), | ||||
|     'alpha': (str, r'[A-Za-z]+'), | ||||
|     'path': (str, r'[^/].*?'), | ||||
| } | ||||
|  | ||||
| ROUTER_CACHE_SIZE = 1024 | ||||
| @@ -71,7 +72,8 @@ class Router: | ||||
|         self.routes_always_check = [] | ||||
|         self.hosts = set() | ||||
|  | ||||
|     def parse_parameter_string(self, parameter_string): | ||||
|     @classmethod | ||||
|     def parse_parameter_string(cls, parameter_string): | ||||
|         """Parse a parameter string into its constituent name, type, and | ||||
|         pattern | ||||
|  | ||||
| @@ -161,10 +163,10 @@ class Router: | ||||
|             parameters.append(parameter) | ||||
|  | ||||
|             # Mark the whole route as unhashable if it has the hash key in it | ||||
|             if re.search('(^|[^^]){1}/', pattern): | ||||
|             if re.search(r'(^|[^^]){1}/', pattern): | ||||
|                 properties['unhashable'] = True | ||||
|             # Mark the route as unhashable if it matches the hash key | ||||
|             elif re.search(pattern, '/'): | ||||
|             elif re.search(r'/', pattern): | ||||
|                 properties['unhashable'] = True | ||||
|  | ||||
|             return '({})'.format(pattern) | ||||
|   | ||||
| @@ -70,7 +70,8 @@ class HttpProtocol(asyncio.Protocol): | ||||
|  | ||||
|     def __init__(self, *, loop, request_handler, error_handler, | ||||
|                  signal=Signal(), connections=set(), request_timeout=60, | ||||
|                  request_max_size=None, request_class=None): | ||||
|                  request_max_size=None, request_class=None, | ||||
|                  keep_alive=True): | ||||
|         self.loop = loop | ||||
|         self.transport = None | ||||
|         self.request = None | ||||
| @@ -88,10 +89,13 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         self._timeout_handler = None | ||||
|         self._last_request_time = None | ||||
|         self._request_handler_task = None | ||||
|         self._keep_alive = keep_alive | ||||
|  | ||||
|     @property | ||||
|     def keep_alive(self): | ||||
|         return self.parser.should_keep_alive() and not self.signal.stopped | ||||
|         return (self._keep_alive | ||||
|                 and not self.signal.stopped | ||||
|                 and self.parser.should_keep_alive()) | ||||
|  | ||||
|     # -------------------------------------------- # | ||||
|     # Connection | ||||
| @@ -322,7 +326,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|           request_timeout=60, ssl=None, sock=None, request_max_size=None, | ||||
|           reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, | ||||
|           register_sys_signals=True, run_async=False, connections=None, | ||||
|           signal=Signal(), request_class=None): | ||||
|           signal=Signal(), request_class=None, keep_alive=True): | ||||
|     """Start asynchronous HTTP Server on an individual process. | ||||
|  | ||||
|     :param host: Address to host on | ||||
| @@ -370,6 +374,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|         request_timeout=request_timeout, | ||||
|         request_max_size=request_max_size, | ||||
|         request_class=request_class, | ||||
|         keep_alive=keep_alive, | ||||
|     ) | ||||
|  | ||||
|     server_coroutine = loop.create_server( | ||||
|   | ||||
| @@ -56,7 +56,7 @@ def register(app, uri, file_or_directory, pattern, | ||||
|         # URL decode the path sent by the browser otherwise we won't be able to | ||||
|         # match filenames which got encoded (filenames with spaces etc) | ||||
|         file_path = path.abspath(unquote(file_path)) | ||||
|         if not file_path.startswith(root_path): | ||||
|         if not file_path.startswith(path.abspath(unquote(root_path))): | ||||
|             raise FileNotFound('File not found', | ||||
|                                path=file_or_directory, | ||||
|                                relative_url=file_uri) | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import sys | ||||
| import signal | ||||
| import asyncio | ||||
| import logging | ||||
|  | ||||
| try: | ||||
|     import ssl | ||||
| except ImportError: | ||||
| @@ -50,8 +51,8 @@ class GunicornWorker(base.Worker): | ||||
|             debug=is_debug, | ||||
|             protocol=protocol, | ||||
|             ssl=self.ssl_context, | ||||
|             run_async=True | ||||
|         ) | ||||
|             run_async=True) | ||||
|         self._server_settings['signal'] = self.signal | ||||
|         self._server_settings.pop('sock') | ||||
|         trigger_events(self._server_settings.get('before_start', []), | ||||
|                        self.loop) | ||||
| @@ -97,7 +98,6 @@ class GunicornWorker(base.Worker): | ||||
|             self.servers.append(await serve( | ||||
|                 sock=sock, | ||||
|                 connections=self.connections, | ||||
|                 signal=self.signal, | ||||
|                 **self._server_settings | ||||
|             )) | ||||
|  | ||||
|   | ||||
| @@ -141,6 +141,16 @@ def test_token(): | ||||
|         return text('OK') | ||||
|  | ||||
|     # uuid4 generated token. | ||||
|     token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' | ||||
|     headers = { | ||||
|         'content-type': 'application/json', | ||||
|         'Authorization': '{}'.format(token) | ||||
|     } | ||||
|  | ||||
|     request, response = app.test_client.get('/', headers=headers) | ||||
|  | ||||
|     assert request.token == token | ||||
|  | ||||
|     token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' | ||||
|     headers = { | ||||
|         'content-type': 'application/json', | ||||
| @@ -151,6 +161,18 @@ def test_token(): | ||||
|  | ||||
|     assert request.token == token | ||||
|  | ||||
|     token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' | ||||
|     headers = { | ||||
|         'content-type': 'application/json', | ||||
|         'Authorization': 'Bearer Token {}'.format(token) | ||||
|     } | ||||
|  | ||||
|     request, response = app.test_client.get('/', headers=headers) | ||||
|  | ||||
|     assert request.token == token | ||||
|  | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| #  POST | ||||
| # ------------------------------------------------------------ # | ||||
|   | ||||
| @@ -238,6 +238,30 @@ def test_dynamic_route_regex(): | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_dynamic_route_path(): | ||||
|     app = Sanic('test_dynamic_route_path') | ||||
|  | ||||
|     @app.route('/<path:path>/info') | ||||
|     async def handler(request, path): | ||||
|         return text('OK') | ||||
|  | ||||
|     request, response = app.test_client.get('/path/1/info') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     request, response = app.test_client.get('/info') | ||||
|     assert response.status == 404 | ||||
|  | ||||
|     @app.route('/<path:path>') | ||||
|     async def handler1(request, path): | ||||
|         return text('OK') | ||||
|  | ||||
|     request, response = app.test_client.get('/info') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|     request, response = app.test_client.get('/whatever/you/set') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_dynamic_route_unhashable(): | ||||
|     app = Sanic('test_dynamic_route_unhashable') | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user