Compare commits
	
		
			242 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 | ||
|   | 18829e648a | ||
|   | a64c636a33 | ||
|   | 5796f211c1 | ||
|   | ae09dec05e | ||
|   | afd51e0823 | ||
|   | 4c66cb1854 | ||
|   | 35b92e1511 | ||
|   | e5d3fe52c5 | ||
|   | 63fe7c0a86 | ||
|   | 235e5511eb | ||
|   | 6b2883074b | ||
|   | 7fe418d1b7 | ||
|   | 0f10a36b40 | ||
|   | 3c45c9170f | ||
|   | a0730aeb44 | ||
|   | e5fdc7fdd0 | ||
|   | 015c87b5e1 | ||
|   | d20a49e500 | ||
|   | adb7331670 | ||
|   | 084f0d27a3 | ||
|   | 522a0beec0 | ||
|   | 77a51c1e05 | ||
|   | 144f215705 | ||
|   | 51b01b6b44 | ||
|   | 09885534c6 | ||
|   | b9dfec38c2 | ||
|   | 2ef8120073 | ||
|   | 52ff2e0e63 | ||
|   | 8cf7dce33f | ||
|   | 9d3bb4a37a | ||
|   | c30437448b | ||
|   | 7e3496f8aa | ||
|   | 46ac79f4dc | ||
|   | 833b14e353 | ||
|   | e9eca25792 | ||
|   | 1854ad133c | ||
|   | 2b5e723ea5 | ||
|   | 9a18906edd | ||
|   | 93cb7582c2 | ||
|   | b4529639f6 | ||
|   | f0a59fccf8 | ||
|   | 46dbaf95a6 | ||
|   | d418b03708 | ||
|   | 765e90ecfa | ||
|   | ff1e88dde6 | ||
|   | 62ebcba647 | ||
|   | 429e90183b | ||
|   | 875790e862 | ||
|   | 25edbe6805 | ||
|   | e148b50d6a | ||
|   | 06d46d56cd | ||
|   | edd8770c67 | ||
|   | daedda8547 | ||
|   | df9d897e75 | ||
|   | fcd8e5e5ad | ||
|   | 6c003f71f4 | ||
|   | 5b704478d9 | ||
|   | 60eb528d68 | ||
|   | 1cf730d957 | ||
|   | 171110b445 | ||
|   | 22699db855 | ||
|   | 18405b3908 | ||
|   | f0a55b5cbb | ||
|   | 04a0774ee5 | ||
|   | 3a8cfb1f45 | ||
|   | dcc19d17d4 | ||
|   | 1ef69adc6f | ||
|   | 75a4df0f32 | ||
|   | 8ba1b5fc35 | ||
|   | a09471ac6c | ||
|   | a916eea684 | ||
|   | 511998d8e1 | ||
|   | e3cf50f791 | ||
|   | 42ba5298a7 | ||
|   | ee79750a22 | ||
|   | 1787f8617f | ||
|   | 748ca28185 | ||
|   | 9c68d713ba | ||
|   | fc69678206 | ||
|   | aebd717039 | ||
|   | 1ddb01ac44 | ||
|   | 3e279cd670 | ||
|   | 724c03630a | ||
|   | b00b2561e5 | ||
|   | c5b50fe3cf | ||
|   | df9884de3c | ||
|   | 65ae7669f9 | ||
|   | 179606feb1 | ||
|   | 536140340e | ||
|   | 5d293df64b | ||
|   | 6188891a53 | ||
|   | 9774661cfe | ||
|   | 563bc34fb5 | ||
|   | 9c95ab3a28 | ||
|   | b776c37b36 | ||
|   | 5b3f92b70f | ||
|   | 1562b81522 | ||
|   | be1016ace6 | ||
|   | ee27c689e1 | ||
|   | fdbf452ced | ||
|   | 3d9927dee0 | ||
|   | 1456b128d2 | ||
|   | 166f77cb86 | ||
|   | 5577838905 | ||
|   | 9c15982299 | ||
|   | 63c24122db | ||
|   | 1396ca903d | ||
|   | d1fb5bdc30 | ||
|   | e27812bf3e | ||
|   | 11a3cf9b99 | ||
|   | a90d70feae | ||
|   | 466b34735c | ||
|   | 7ca9116e37 | ||
|   | decd3e737c | ||
|   | f35442ad1b | ||
|   | 2b296435b3 | ||
|   | 19ee1dfecc | ||
|   | 7da4596ef8 | ||
|   | a379ef6781 | ||
|   | 7beb065be3 | ||
|   | 38b9091513 | ||
|   | 96db3c9601 | ||
|   | 43c4fc8e33 | ||
|   | 986ff101ce | ||
|   | 94c83c445f | ||
|   | 625865412f | ||
|   | 46677e69ce | ||
|   | 5fbca5b823 | ||
|   | 879fab120f | ||
|   | 391b24bc17 | ||
|   | d713533d26 | ||
|   | 24f745a334 | ||
|   | 86f3101861 | ||
|   | fd823c63ab | ||
|   | fa69892f70 | ||
|   | cfc53d0d26 | ||
|   | 97c2056e4a | ||
|   | 0ad0164171 | ||
|   | df0e285b6f | ||
|   | e92f1b8c28 | ||
|   | 410f86c960 | ||
|   | 85f27320e7 | ||
|   | 9a3fac90e1 | ||
|   | 6984f6eec4 | ||
|   | d05f502fc8 | ||
|   | ba41ab8f67 | ||
|   | 250bb7e29d | ||
|   | 48a26fd5df | ||
|   | 3af26540ec | ||
|   | 7d9de068d9 | ||
|   | d174917a07 | ||
|   | af398fc4c4 | ||
|   | 878ef446a2 | ||
|   | 668f6477bb | ||
|   | 01a770cbca | ||
|   | 23a1174aa2 | ||
|   | 414020e75b | ||
|   | ed74bccad6 | ||
|   | 0eedde445c | ||
|   | 88bf78213f | ||
|   | d342461a51 | ||
|   | dffaaf8751 | ||
|   | 313edadf47 | ||
|   | c9ce33dfe6 | ||
|   | 0f50ac7205 | ||
|   | e807c08275 | ||
|   | 893977365c | ||
|   | 0cac45809f | ||
|   | 489ca3c207 | ||
|   | 313535c599 | ||
|   | 7f1e0557c9 | ||
|   | 0860f84a39 | ||
|   | 2fe9e78b6d | ||
|   | 2ba30f2022 | ||
|   | b3b27cab34 | ||
|   | 694207a86d | ||
|   | 90138c4bae | ||
|   | 58a833e987 | ||
|   | 86c5a569d5 | ||
|   | 19592e8eea | ||
|   | 8e6678d526 | ||
|   | e792a1e030 | ||
|   | f0e818a28c | ||
|   | 69bd63b742 | ||
|   | b40f30f2e5 | ||
|   | 1fbde87ec2 | ||
|   | f9dc34c8fa | ||
|   | f7186f5331 | ||
|   | 6a680e4db0 | ||
|   | f6b69f412f | ||
|   | 5aed18862d | ||
|   | 62bf213a6e | ||
|   | e5c32e9b48 | ||
|   | b87dc37fbb | ||
|   | 002d4cb37c | ||
|   | ff321fc355 | ||
|   | c3386dec84 | ||
|   | 927d2761f7 | ||
|   | 3289e8403a | ||
|   | 104a7c7d05 | ||
|   | 7560660ec7 | ||
|   | 40ccb4a0dd | ||
|   | f90288f5dc | ||
|   | 3bf79898d9 | ||
|   | 1d6e11ca10 | ||
|   | 6e903ee7d5 | ||
|   | 2dca53a696 | ||
|   | d8a6d7e02f | ||
|   | 1a8961587c | ||
|   | fa13ad8849 | ||
|   | 8b23dec322 | ||
|   | 4e8aac4b41 | ||
|   | e6a828572a | ||
|   | 6ea43d8e6d | 
							
								
								
									
										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. | ||||
							
								
								
									
										6
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| FROM python:3.6 | ||||
|  | ||||
| ADD . /app | ||||
| WORKDIR /app | ||||
|  | ||||
| RUN pip install tox | ||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| test: | ||||
| 	find . -name "*.pyc" -delete | ||||
| 	docker build -t sanic/test-image . | ||||
| 	docker run -t sanic/test-image tox | ||||
| @@ -59,6 +59,13 @@ Installation | ||||
|  | ||||
| -  ``python -m pip install sanic`` | ||||
|  | ||||
| To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables | ||||
| using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features | ||||
| installation. | ||||
|  | ||||
| - ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic`` | ||||
|  | ||||
|  | ||||
| Documentation | ||||
| ------------- | ||||
|  | ||||
|   | ||||
							
								
								
									
										27
									
								
								docs/conf.py
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								docs/conf.py
									
									
									
									
									
								
							| @@ -22,7 +22,7 @@ import sanic | ||||
|  | ||||
| # -- General configuration ------------------------------------------------ | ||||
|  | ||||
| extensions = [] | ||||
| extensions = ['sphinx.ext.autodoc'] | ||||
|  | ||||
| templates_path = ['_templates'] | ||||
|  | ||||
| @@ -68,7 +68,6 @@ pygments_style = 'sphinx' | ||||
| # If true, `todo` and `todoList` produce output, else they produce nothing. | ||||
| todo_include_todos = False | ||||
|  | ||||
|  | ||||
| # -- Options for HTML output ---------------------------------------------- | ||||
|  | ||||
| # The theme to use for HTML and HTML Help pages.  See the documentation for | ||||
| @@ -80,13 +79,11 @@ html_theme = 'sphinx_rtd_theme' | ||||
| # so a file named "default.css" will overwrite the builtin "default.css". | ||||
| html_static_path = ['_static'] | ||||
|  | ||||
|  | ||||
| # -- Options for HTMLHelp output ------------------------------------------ | ||||
|  | ||||
| # Output file base name for HTML help builder. | ||||
| htmlhelp_basename = 'Sanicdoc' | ||||
|  | ||||
|  | ||||
| # -- Options for LaTeX output --------------------------------------------- | ||||
|  | ||||
| latex_elements = { | ||||
| @@ -110,21 +107,14 @@ latex_elements = { | ||||
| # Grouping the document tree into LaTeX files. List of tuples | ||||
| # (source start file, target name, title, | ||||
| #  author, documentclass [howto, manual, or own class]). | ||||
| latex_documents = [ | ||||
|     (master_doc, 'Sanic.tex', 'Sanic Documentation', | ||||
|      'Sanic contributors', 'manual'), | ||||
| ] | ||||
|  | ||||
| latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation', | ||||
|                     'Sanic contributors', 'manual'), ] | ||||
|  | ||||
| # -- Options for manual page output --------------------------------------- | ||||
|  | ||||
| # One entry per manual page. List of tuples | ||||
| # (source start file, name, description, authors, manual section). | ||||
| man_pages = [ | ||||
|     (master_doc, 'sanic', 'Sanic Documentation', | ||||
|      [author], 1) | ||||
| ] | ||||
|  | ||||
| man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)] | ||||
|  | ||||
| # -- Options for Texinfo output ------------------------------------------- | ||||
|  | ||||
| @@ -132,13 +122,10 @@ man_pages = [ | ||||
| # (source start file, target name, title, author, | ||||
| #  dir menu entry, description, category) | ||||
| texinfo_documents = [ | ||||
|     (master_doc, 'Sanic', 'Sanic Documentation', | ||||
|      author, 'Sanic', 'One line description of project.', | ||||
|      'Miscellaneous'), | ||||
|     (master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic', | ||||
|      'One line description of project.', 'Miscellaneous'), | ||||
| ] | ||||
|  | ||||
|  | ||||
|  | ||||
| # -- Options for Epub output ---------------------------------------------- | ||||
|  | ||||
| # Bibliographic Dublin Core info. | ||||
| @@ -150,8 +137,6 @@ epub_copyright = copyright | ||||
| # A list of files that should not be packed into the epub file. | ||||
| epub_exclude_files = ['search.html'] | ||||
|  | ||||
|  | ||||
|  | ||||
| # -- Custom Settings ------------------------------------------------------- | ||||
|  | ||||
| suppress_warnings = ['image.nonlocal_uri'] | ||||
|   | ||||
| @@ -9,12 +9,14 @@ Guides | ||||
|    sanic/getting_started | ||||
|    sanic/routing | ||||
|    sanic/request_data | ||||
|    sanic/response | ||||
|    sanic/static_files | ||||
|    sanic/exceptions | ||||
|    sanic/middleware | ||||
|    sanic/blueprints | ||||
|    sanic/config | ||||
|    sanic/cookies | ||||
|    sanic/streaming | ||||
|    sanic/class_based_views | ||||
|    sanic/custom_protocol | ||||
|    sanic/ssl | ||||
|   | ||||
| @@ -55,13 +55,18 @@ will look like: | ||||
|  | ||||
| Blueprints have much the same functionality as an application instance. | ||||
|  | ||||
| ### WebSocket routes | ||||
|  | ||||
| WebSocket handlers can be registered on a blueprint using the `@bp.websocket` | ||||
| decorator or `bp.add_websocket_route` method. | ||||
|  | ||||
| ### Middleware | ||||
|  | ||||
| Using blueprints allows you to also register middleware globally. | ||||
|  | ||||
| ```python | ||||
| @bp.middleware | ||||
| async def halt_request(request): | ||||
| async def print_on_request(request): | ||||
| 	print("I am a spy") | ||||
|  | ||||
| @bp.middleware('request') | ||||
|   | ||||
| @@ -48,6 +48,24 @@ app.add_route(SimpleView.as_view(), '/') | ||||
|  | ||||
| ``` | ||||
|  | ||||
| You can also use `async` syntax. | ||||
|  | ||||
| ```python | ||||
| from sanic import Sanic | ||||
| from sanic.views import HTTPMethodView | ||||
| from sanic.response import text | ||||
|  | ||||
| app = Sanic('some_name') | ||||
|  | ||||
| class SimpleAsyncView(HTTPMethodView): | ||||
|  | ||||
|   async def get(self, request): | ||||
|       return text('I am async get method') | ||||
|  | ||||
| app.add_route(SimpleAsyncView.as_view(), '/') | ||||
|  | ||||
| ``` | ||||
|  | ||||
| ## URL parameters | ||||
|  | ||||
| If you need any URL parameters, as discussed in the routing guide, include them | ||||
|   | ||||
| @@ -29,6 +29,14 @@ In general the convention is to only have UPPERCASE configuration parameters. Th | ||||
| 
 | ||||
| There are several ways how to load configuration. | ||||
| 
 | ||||
| ### From environment variables. | ||||
| 
 | ||||
| Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_vars` boolean to the Sanic constructor to override that: | ||||
| 
 | ||||
| ```python | ||||
| app = Sanic(load_vars=False) | ||||
| ``` | ||||
| 
 | ||||
| ### From an Object | ||||
| 
 | ||||
| If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module: | ||||
| @@ -75,4 +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 | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ both read and write cookies, which are stored as key-value pairs. | ||||
|  | ||||
| ## Reading cookies | ||||
|  | ||||
| A user's cookies can be accessed `Request` object's `cookie` dictionary. | ||||
| A user's cookies can be accessed via the `Request` object's `cookies` dictionary. | ||||
|  | ||||
| ```python | ||||
| from sanic.response import text | ||||
|   | ||||
							
								
								
									
										39
									
								
								docs/sanic/decorators.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								docs/sanic/decorators.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| # Handler Decorators | ||||
|  | ||||
| Since Sanic handlers are simple Python functions, you can apply decorators to them in a similar manner to Flask. A typical use case is when you want some code to run before a handler's code is executed.  | ||||
|  | ||||
| ## Authorization Decorator | ||||
|  | ||||
| Let's say you want to check that a user is authorized to access a particular endpoint. You can create a decorator that wraps a handler function, checks a request if the client is authorized to access a resource, and sends the appropriate response. | ||||
|  | ||||
|  | ||||
| ```python | ||||
| from functools import wraps | ||||
| from sanic.response import json | ||||
|  | ||||
| def authorized(): | ||||
|     def decorator(f): | ||||
|         @wraps(f) | ||||
|         async def decorated_function(request, *args, **kwargs): | ||||
|             # run some method that checks the request | ||||
|             # for the client's authorization status | ||||
|             is_authorized = check_request_for_authorization_status(request) | ||||
|  | ||||
|             if is_authorized: | ||||
|                 # the user is authorized. | ||||
|                 # run the handler method and return the response | ||||
|                 response = await f(request, *args, **kwargs) | ||||
|                 return response | ||||
|             else: | ||||
|                 # the user is not authorized.  | ||||
|                 return json({'status': 'not_authorized'}, 403) | ||||
|         return decorated_function | ||||
|     return decorator | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| @authorized() | ||||
| async def test(request): | ||||
|     return json({status: 'authorized'}) | ||||
| ```  | ||||
|  | ||||
| @@ -44,3 +44,15 @@ directly run by the interpreter. | ||||
| if __name__ == '__main__': | ||||
|     app.run(host='0.0.0.0', port=1337, workers=4) | ||||
| ``` | ||||
|  | ||||
| ## Running via Gunicorn | ||||
|  | ||||
| [Gunicorn](http://gunicorn.org/) ‘Green Unicorn’ is a WSGI HTTP Server for UNIX. | ||||
| It’s a pre-fork worker model ported from Ruby’s Unicorn project. | ||||
|  | ||||
| In order to run Sanic application with Gunicorn, you need to use the special `sanic.worker.GunicornWorker` | ||||
| for Gunicorn `worker-class` argument: | ||||
|  | ||||
| ``` | ||||
| gunicorn --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker | ||||
| ``` | ||||
|   | ||||
| @@ -12,3 +12,13 @@ A list of Sanic extensions created by the community. | ||||
| - [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper. | ||||
| - [Sanic CRUD](https://github.com/Typhon66/sanic_crud): CRUD REST API generation with peewee models. | ||||
| - [UserAgent](https://github.com/lixxu/sanic-useragent): Add `user_agent` to request | ||||
| - [Limiter](https://github.com/bohea/sanic-limiter): Rate limiting for sanic. | ||||
| - [Sanic EnvConfig](https://github.com/jamesstidard/sanic-envconfig): Pull environment variables into your sanic config. | ||||
| - [Babel](https://github.com/lixxu/sanic-babel): Adds i18n/l10n support to Sanic applications with the help of the | ||||
| `Babel` library | ||||
| - [Dispatch](https://github.com/ashleysommer/sanic-dispatcher): A dispatcher inspired by `DispatcherMiddleware` in werkzeug. Can act as a Sanic-to-WSGI adapter. | ||||
| - [Sanic-OAuth](https://github.com/Sniedes722/Sanic-OAuth): OAuth Library for connecting to & creating your own token providers. | ||||
| - [Sanic-nginx-docker-example](https://github.com/itielshwartz/sanic-nginx-docker-example): Simple and easy to use example of Sanic behined nginx using docker-compose. | ||||
| - [sanic-graphql](https://github.com/graphql-python/sanic-graphql): GraphQL integration with Sanic | ||||
| - [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic | ||||
| - [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation. | ||||
|   | ||||
| @@ -7,8 +7,8 @@ On top of being Flask-like, Sanic supports async request handlers.  This means y | ||||
|  | ||||
| Sanic is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome! | ||||
|  | ||||
| Sanic aspires to be simple: | ||||
| ------------------- | ||||
| Sanic aspires to be simple | ||||
| --------------------------- | ||||
|  | ||||
| .. code:: python | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,13 @@ | ||||
| # Middleware | ||||
| # Middleware And Listeners | ||||
|  | ||||
| Middleware are functions which are executed before or after requests to the | ||||
| server. They can be used to modify the *request to* or *response from* | ||||
| user-defined handler functions. | ||||
|  | ||||
| Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle. | ||||
|  | ||||
| ## Middleware | ||||
|  | ||||
| There are two types of middleware: request and response. Both are declared | ||||
| using the `@app.middleware` decorator, with the decorator's parameter being a | ||||
| string representing its type: `'request'` or `'response'`. Response middleware | ||||
| @@ -64,3 +68,45 @@ async def halt_request(request): | ||||
| async def halt_response(request, response): | ||||
| 	return text('I halted the response') | ||||
| ``` | ||||
|  | ||||
| ## Listeners | ||||
|  | ||||
| If you want to execute startup/teardown code as your server starts or closes, you can use the following listeners: | ||||
|  | ||||
| - `before_server_start` | ||||
| - `after_server_start` | ||||
| - `before_server_stop` | ||||
| - `after_server_stop` | ||||
|  | ||||
| These listeners are implemented as decorators on functions which accept the app object as well as the asyncio loop.  | ||||
|  | ||||
| For example: | ||||
|  | ||||
| ```python | ||||
| @app.listener('before_server_start') | ||||
| async def setup_db(app, loop): | ||||
|     app.db = await db_setup() | ||||
|  | ||||
| @app.listener('after_server_start') | ||||
| async def notify_server_started(app, loop): | ||||
|     print('Server successfully started!') | ||||
|  | ||||
| @app.listener('before_server_stop') | ||||
| async def notify_server_stopping(app, loop): | ||||
|     print('Server shutting down!') | ||||
|  | ||||
| @app.listener('after_server_stop') | ||||
| async def close_db(app, loop): | ||||
|     await app.db.close() | ||||
| ``` | ||||
|  | ||||
| If you want to schedule a background task to run after the loop has started, | ||||
| Sanic provides the `add_task` method to easily do so. | ||||
|  | ||||
| ```python | ||||
| async def notify_server_started_after_five_seconds(): | ||||
|     await asyncio.sleep(5) | ||||
|     print('Server successfully started!') | ||||
|  | ||||
| app.add_task(notify_server_started_after_five_seconds()) | ||||
| ``` | ||||
|   | ||||
| @@ -17,7 +17,7 @@ The following variables are accessible as properties on `Request` objects: | ||||
|  | ||||
| - `args` (dict) - Query string variables. A query string is the section of a | ||||
|   URL that resembles `?key1=value1&key2=value2`. If that URL were to be parsed, | ||||
|   the `args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`. | ||||
|   the `args` dictionary would look like `{'key1': ['value1'], 'key2': ['value2']}`. | ||||
|   The request's `query_string` variable holds the unparsed string value. | ||||
|  | ||||
|   ```python | ||||
| @@ -28,6 +28,10 @@ The following variables are accessible as properties on `Request` objects: | ||||
|       return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string }) | ||||
|   ``` | ||||
|  | ||||
| - `raw_args` (dict) - On many cases you would need to access the url arguments in | ||||
|   a less packed dictionary. For same previous URL `?key1=value1&key2=value2`, the | ||||
|   `raw_args` dictionary would look like `{'key1': 'value1', 'key2': 'value2'}`. | ||||
|  | ||||
| - `files` (dictionary of `File` objects) - List of files that have a name, body, and type | ||||
|  | ||||
|   ```python | ||||
| @@ -85,6 +89,12 @@ The following variables are accessible as properties on `Request` objects: | ||||
|           return json({'status': 'production'}) | ||||
|  | ||||
|   ``` | ||||
| - `url`: The full URL of the request, ie: `http://localhost:8000/posts/1/?foo=bar` | ||||
| - `scheme`: The URL scheme associated with the request: `http` or `https` | ||||
| - `host`: The host associated with the request: `localhost:8080` | ||||
| - `path`: The path of the request: `/posts/1/` | ||||
| - `query_string`: The query string of the request: `foo=bar` or a blank string `''` | ||||
|  | ||||
|  | ||||
| ## Accessing values using `get` and `getlist` | ||||
|  | ||||
|   | ||||
							
								
								
									
										102
									
								
								docs/sanic/response.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								docs/sanic/response.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| # Response | ||||
|  | ||||
| Use functions in `sanic.response` module to create responses. | ||||
|  | ||||
| ## Plain Text | ||||
|  | ||||
| ```python | ||||
| from sanic import response | ||||
|  | ||||
|  | ||||
| @app.route('/text') | ||||
| def handle_request(request): | ||||
|     return response.text('Hello world!') | ||||
| ``` | ||||
|  | ||||
| ## HTML | ||||
|  | ||||
| ```python | ||||
| from sanic import response | ||||
|  | ||||
|  | ||||
| @app.route('/html') | ||||
| def handle_request(request): | ||||
|     return response.html('<p>Hello world!</p>') | ||||
| ``` | ||||
|  | ||||
| ## JSON | ||||
|  | ||||
|  | ||||
| ```python | ||||
| from sanic import response | ||||
|  | ||||
|  | ||||
| @app.route('/json') | ||||
| def handle_request(request): | ||||
|     return response.json({'message': 'Hello world!'}) | ||||
| ``` | ||||
|  | ||||
| ## File | ||||
|  | ||||
| ```python | ||||
| from sanic import response | ||||
|  | ||||
|  | ||||
| @app.route('/file') | ||||
| async def handle_request(request): | ||||
|     return await response.file('/srv/www/whatever.png') | ||||
| ``` | ||||
|  | ||||
| ## Streaming | ||||
|  | ||||
| ```python | ||||
| from sanic import response | ||||
|  | ||||
| @app.route("/streaming") | ||||
| async def index(request): | ||||
|     async def streaming_fn(response): | ||||
|         response.write('foo') | ||||
|         response.write('bar') | ||||
|     return response.stream(streaming_fn, content_type='text/plain') | ||||
| ``` | ||||
|  | ||||
| ## Redirect | ||||
|  | ||||
| ```python | ||||
| from sanic import response | ||||
|  | ||||
|  | ||||
| @app.route('/redirect') | ||||
| def handle_request(request): | ||||
|     return response.redirect('/json') | ||||
| ``` | ||||
|  | ||||
| ## Raw | ||||
|  | ||||
| Response without encoding the body | ||||
|  | ||||
| ```python | ||||
| from sanic import response | ||||
|  | ||||
|  | ||||
| @app.route('/raw') | ||||
| def handle_request(request): | ||||
|     return response.raw('raw data') | ||||
| ``` | ||||
|  | ||||
| ## Modify headers or status | ||||
|  | ||||
| To modify headers or status code, pass the `headers` or `status` argument to those functions: | ||||
|  | ||||
| ```python | ||||
| from sanic import response | ||||
|  | ||||
|  | ||||
| @app.route('/json') | ||||
| def handle_request(request): | ||||
|     return response.json( | ||||
|         {'message': 'Hello world!'}, | ||||
|         headers={'X-Served-By': 'sanic'}, | ||||
|         status=200 | ||||
|     ) | ||||
| ``` | ||||
| @@ -181,3 +181,37 @@ url = app.url_for('post_handler', post_id=5, arg_one=['one', 'two'], arg_two=2, | ||||
| # http://another_server:8888/posts/5?arg_one=one&arg_one=two&arg_two=2#anchor | ||||
| ``` | ||||
| - All valid parameters must be passed to `url_for` to build a URL. If a parameter is not supplied, or if a parameter does not match the specified type, a `URLBuildError` will be thrown. | ||||
|  | ||||
| ## WebSocket routes | ||||
|  | ||||
| Routes for the WebSocket protocol can be defined with the `@app.websocket` | ||||
| decorator: | ||||
|  | ||||
| ```python | ||||
| @app.websocket('/feed') | ||||
| async def feed(request, ws): | ||||
|     while True: | ||||
|         data = 'hello!' | ||||
|         print('Sending: ' + data) | ||||
|         await ws.send(data) | ||||
|         data = await ws.recv() | ||||
|         print('Received: ' + data) | ||||
| ``` | ||||
|  | ||||
| Alternatively, the `app.add_websocket_route` method can be used instead of the | ||||
| decorator: | ||||
|  | ||||
| ```python | ||||
| async def feed(request, ws): | ||||
|     pass | ||||
|  | ||||
| app.add_websocket_route(my_websocket_handler, '/feed') | ||||
| ``` | ||||
|  | ||||
| Handlers for a WebSocket route are passed the request as first argument, and a | ||||
| WebSocket protocol object as second argument. The protocol object has `send` | ||||
| and `recv` methods to send and receive data respectively. | ||||
|  | ||||
| WebSocket support requires the [websockets](https://github.com/aaugustin/websockets) | ||||
| package by Aymeric Augustin. | ||||
|  | ||||
|   | ||||
| @@ -10,3 +10,11 @@ Optionally pass in an SSLContext: | ||||
|   context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile") | ||||
|  | ||||
|   app.run(host="0.0.0.0", port=8443, ssl=context) | ||||
|  | ||||
| You can also pass in the locations of a certificate and key as a dictionary: | ||||
|  | ||||
|  | ||||
| .. code:: python | ||||
|  | ||||
|   ssl = {'cert': "/path/to/cert", 'key': "/path/to/keyfile"} | ||||
|   app.run(host="0.0.0.0", port=8443, ssl=ssl) | ||||
|   | ||||
							
								
								
									
										32
									
								
								docs/sanic/streaming.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								docs/sanic/streaming.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| # Streaming | ||||
|  | ||||
| Sanic allows you to stream content to the client with the `stream` method. This method accepts a coroutine callback which is passed a `StreamingHTTPResponse` object that is written to. A simple example is like follows: | ||||
|  | ||||
| ```python | ||||
| from sanic import Sanic | ||||
| from sanic.response import stream | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| @app.route("/") | ||||
| async def test(request): | ||||
|     async def sample_streaming_fn(response): | ||||
|         response.write('foo,') | ||||
|         response.write('bar') | ||||
|  | ||||
|     return stream(sample_streaming_fn, content_type='text/csv') | ||||
| ``` | ||||
|  | ||||
| This is useful in situations where you want to stream content to the client that originates in an external service, like a database. For example, you can stream database records to the client with the asynchronous cursor that `asyncpg` provides: | ||||
|  | ||||
| ```python | ||||
| @app.route("/") | ||||
| async def index(request): | ||||
|     async def stream_from_db(response): | ||||
|         conn = await asyncpg.connect(database='test') | ||||
|         async with conn.transaction(): | ||||
|             async for record in conn.cursor('SELECT generate_series(0, 10)'): | ||||
|                 response.write(record[0]) | ||||
|  | ||||
|     return stream(stream_from_db) | ||||
| ``` | ||||
| @@ -15,4 +15,5 @@ dependencies: | ||||
|   - httptools>=0.0.9 | ||||
|   - ujson>=1.35 | ||||
|   - aiofiles>=0.3.0 | ||||
|   - websockets>=3.2 | ||||
|   - https://github.com/channelcat/docutils-fork/zipball/master | ||||
| @@ -1,5 +1,5 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| from sanic import response | ||||
|  | ||||
| import aiohttp | ||||
|  | ||||
| @@ -9,20 +9,18 @@ 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() | ||||
|     async with session.get(url) as result: | ||||
|         return await result.json() | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| async def test(request): | ||||
|     """ | ||||
|     Download and serve example JSON | ||||
|     """ | ||||
| @app.route('/') | ||||
| async def handle_request(request): | ||||
|     url = "https://api.github.com/repos/channelcat/sanic" | ||||
|      | ||||
|     async with aiohttp.ClientSession() as session: | ||||
|         response = await fetch(session, url) | ||||
|         return json(response) | ||||
|         result = await fetch(session, url) | ||||
|         return response.json(result) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="0.0.0.0", port=8000, workers=2) | ||||
|   | ||||
							
								
								
									
										0
									
								
								examples/asyncorm/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								examples/asyncorm/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										140
									
								
								examples/asyncorm/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								examples/asyncorm/__main__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.exceptions import NotFound | ||||
| from sanic.response import json | ||||
| from sanic.views import HTTPMethodView | ||||
|  | ||||
| from asyncorm import configure_orm | ||||
| from asyncorm.exceptions import QuerysetError | ||||
|  | ||||
| from library.models import Book | ||||
| from library.serializer import BookSerializer | ||||
|  | ||||
| app = Sanic(name=__name__) | ||||
|  | ||||
|  | ||||
| @app.listener('before_server_start') | ||||
| def orm_configure(sanic, loop): | ||||
|     db_config = {'database': 'sanic_example', | ||||
|                  'host': 'localhost', | ||||
|                  'user': 'sanicdbuser', | ||||
|                  'password': 'sanicDbPass', | ||||
|                  } | ||||
|  | ||||
|     # configure_orm needs a dictionary with: | ||||
|     #    * the database configuration | ||||
|     #    * the application/s where the models are defined | ||||
|     orm_app = configure_orm({'loop': loop,  # always use the sanic loop! | ||||
|                              'db_config': db_config, | ||||
|                              'modules': ['library', ],  # list of apps | ||||
|                              }) | ||||
|  | ||||
|     # orm_app is the object that orchestrates the whole ORM | ||||
|     # sync_db should be run only once, better do that as external command | ||||
|     # it creates the tables in the database!!!! | ||||
|     # orm_app.sync_db() | ||||
|  | ||||
|  | ||||
| # for all the 404 lets handle the exceptions | ||||
| @app.exception(NotFound) | ||||
| def ignore_404s(request, exception): | ||||
|     return json({'method': request.method, | ||||
|                  'status': exception.status_code, | ||||
|                  'error': exception.args[0], | ||||
|                  'results': None, | ||||
|                  }) | ||||
|  | ||||
|  | ||||
| # now the propper sanic workflow | ||||
| class BooksView(HTTPMethodView): | ||||
|     def arg_parser(self, request): | ||||
|         parsed_args = {} | ||||
|         for k, v in request.args.items(): | ||||
|             parsed_args[k] = v[0] | ||||
|         return parsed_args | ||||
|  | ||||
|     async def get(self, request): | ||||
|         filtered_by = self.arg_parser(request) | ||||
|  | ||||
|         if filtered_by: | ||||
|             q_books = await Book.objects.filter(**filtered_by) | ||||
|         else: | ||||
|             q_books = await Book.objects.all() | ||||
|  | ||||
|         books = [BookSerializer.serialize(book) for book in q_books] | ||||
|  | ||||
|         return json({'method': request.method, | ||||
|                      'status': 200, | ||||
|                      'results': books or None, | ||||
|                      'count': len(books), | ||||
|                      }) | ||||
|  | ||||
|     async def post(self, request): | ||||
|         # populate the book with the data in the request | ||||
|         book = Book(**request.json) | ||||
|  | ||||
|         # and await on save | ||||
|         await book.save() | ||||
|  | ||||
|         return json({'method': request.method, | ||||
|                      'status': 201, | ||||
|                      'results': BookSerializer.serialize(book), | ||||
|                      }) | ||||
|  | ||||
|  | ||||
| class BookView(HTTPMethodView): | ||||
|     async def get_object(self, request, book_id): | ||||
|         try: | ||||
|             # await on database consults | ||||
|             book = await Book.objects.get(**{'id': book_id}) | ||||
|         except QuerysetError as e: | ||||
|             raise NotFound(e.args[0]) | ||||
|         return book | ||||
|  | ||||
|     async def get(self, request, book_id): | ||||
|         # await on database consults | ||||
|         book = await self.get_object(request, book_id) | ||||
|  | ||||
|         return json({'method': request.method, | ||||
|                      'status': 200, | ||||
|                      'results': BookSerializer.serialize(book), | ||||
|                      }) | ||||
|  | ||||
|     async def put(self, request, book_id): | ||||
|         # await on database consults | ||||
|         book = await self.get_object(request, book_id) | ||||
|         # await on save | ||||
|         await book.save(**request.json) | ||||
|  | ||||
|         return json({'method': request.method, | ||||
|                      'status': 200, | ||||
|                      'results': BookSerializer.serialize(book), | ||||
|                      }) | ||||
|  | ||||
|     async def patch(self, request, book_id): | ||||
|         # await on database consults | ||||
|         book = await self.get_object(request, book_id) | ||||
|         # await on save | ||||
|         await book.save(**request.json) | ||||
|  | ||||
|         return json({'method': request.method, | ||||
|                      'status': 200, | ||||
|                      'results': BookSerializer.serialize(book), | ||||
|                      }) | ||||
|  | ||||
|     async def delete(self, request, book_id): | ||||
|         # await on database consults | ||||
|         book = await self.get_object(request, book_id) | ||||
|         # await on its deletion | ||||
|         await book.delete() | ||||
|  | ||||
|         return json({'method': request.method, | ||||
|                      'status': 200, | ||||
|                      'results': None | ||||
|                      }) | ||||
|  | ||||
|  | ||||
| app.add_route(BooksView.as_view(), '/books/') | ||||
| app.add_route(BookView.as_view(), '/books/<book_id:int>/') | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run() | ||||
							
								
								
									
										0
									
								
								examples/asyncorm/library/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								examples/asyncorm/library/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								examples/asyncorm/library/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								examples/asyncorm/library/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| from asyncorm.model import Model | ||||
| from asyncorm.fields import CharField, IntegerField, DateField | ||||
|  | ||||
|  | ||||
| BOOK_CHOICES = ( | ||||
|     ('hard cover', 'hard cover book'), | ||||
|     ('paperback', 'paperback book') | ||||
| ) | ||||
|  | ||||
|  | ||||
| # This is a simple model definition | ||||
| class Book(Model): | ||||
|     name = CharField(max_length=50) | ||||
|     synopsis = CharField(max_length=255) | ||||
|     book_type = CharField(max_length=15, null=True, choices=BOOK_CHOICES) | ||||
|     pages = IntegerField(null=True) | ||||
|     date_created = DateField(auto_now=True) | ||||
|  | ||||
|     class Meta(): | ||||
|         ordering = ['name', ] | ||||
|         unique_together = ['name', 'synopsis'] | ||||
							
								
								
									
										15
									
								
								examples/asyncorm/library/serializer.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								examples/asyncorm/library/serializer.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| from asyncorm.model import ModelSerializer, SerializerMethod | ||||
| from library.models import Book | ||||
|  | ||||
|  | ||||
| class BookSerializer(ModelSerializer): | ||||
|     book_type = SerializerMethod() | ||||
|  | ||||
|     def get_book_type(self, instance): | ||||
|         return instance.book_type_display() | ||||
|  | ||||
|     class Meta(): | ||||
|         model = Book | ||||
|         fields = [ | ||||
|             'id', 'name', 'synopsis', 'book_type', 'pages', 'date_created' | ||||
|         ] | ||||
							
								
								
									
										2
									
								
								examples/asyncorm/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								examples/asyncorm/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| asyncorm==0.0.7 | ||||
| sanic==0.4.1 | ||||
| @@ -6,6 +6,7 @@ from sanic.response import json, text | ||||
| app = Sanic(__name__) | ||||
| blueprint = Blueprint('name', url_prefix='/my_blueprint') | ||||
| blueprint2 = Blueprint('name', url_prefix='/my_blueprint2') | ||||
| blueprint3 = Blueprint('name', url_prefix='/my_blueprint3') | ||||
|  | ||||
|  | ||||
| @blueprint.route('/foo') | ||||
| @@ -17,8 +18,17 @@ async def foo(request): | ||||
| async def foo2(request): | ||||
|     return json({'msg': 'hi from blueprint2'}) | ||||
|  | ||||
| @blueprint3.websocket('/foo') | ||||
| async def foo3(request, ws): | ||||
|     while True: | ||||
|         data = 'hello!' | ||||
|         print('Sending: ' + data) | ||||
|         await ws.send(data) | ||||
|         data = await ws.recv() | ||||
|         print('Received: ' + data) | ||||
|  | ||||
| app.register_blueprint(blueprint) | ||||
| app.register_blueprint(blueprint2) | ||||
| app.blueprint(blueprint) | ||||
| app.blueprint(blueprint2) | ||||
| app.blueprint(blueprint3) | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000, debug=True) | ||||
|   | ||||
							
								
								
									
										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) | ||||
							
								
								
									
										136
									
								
								examples/detailed_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								examples/detailed_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| # This demo requires aioredis and environmental variables established in ENV_VARS | ||||
| import json | ||||
| import logging | ||||
| import os | ||||
|  | ||||
| from datetime import datetime | ||||
|  | ||||
| import aioredis | ||||
|  | ||||
| import sanic | ||||
| from sanic import Sanic | ||||
|  | ||||
|  | ||||
| ENV_VARS = ["REDIS_HOST", "REDIS_PORT", | ||||
|             "REDIS_MINPOOL", "REDIS_MAXPOOL", | ||||
|             "REDIS_PASS", "APP_LOGFILE"] | ||||
|  | ||||
| app = Sanic(name=__name__) | ||||
|  | ||||
| logger = None | ||||
|  | ||||
|  | ||||
| @app.middleware("request") | ||||
| async def log_uri(request): | ||||
|     # Simple middleware to log the URI endpoint that was called | ||||
|     logger.info("URI called: {0}".format(request.url)) | ||||
|  | ||||
|  | ||||
| @app.listener('before_server_start') | ||||
| async def before_server_start(app, loop): | ||||
|     logger.info("Starting redis pool") | ||||
|     app.redis_pool = await aioredis.create_pool( | ||||
|         (app.config.REDIS_HOST, int(app.config.REDIS_PORT)), | ||||
|         minsize=int(app.config.REDIS_MINPOOL), | ||||
|         maxsize=int(app.config.REDIS_MAXPOOL), | ||||
|         password=app.config.REDIS_PASS) | ||||
|  | ||||
|  | ||||
| @app.listener('after_server_stop') | ||||
| async def after_server_stop(app, loop): | ||||
|     logger.info("Closing redis pool") | ||||
|     app.redis_pool.close() | ||||
|     await app.redis_pool.wait_closed() | ||||
|  | ||||
|  | ||||
| @app.middleware("request") | ||||
| async def attach_db_connectors(request): | ||||
|     # Just put the db objects in the request for easier access | ||||
|     logger.info("Passing redis pool to request object") | ||||
|     request["redis"] = request.app.redis_pool | ||||
|  | ||||
|  | ||||
| @app.route("/state/<user_id>", methods=["GET"]) | ||||
| async def access_state(request, user_id): | ||||
|     try: | ||||
|         # Check to see if the value is in cache, if so lets return that | ||||
|         with await request["redis"] as redis_conn: | ||||
|             state = await redis_conn.get(user_id, encoding="utf-8") | ||||
|             if state: | ||||
|                 return sanic.response.json({"msg": "Success", | ||||
|                                             "status": 200, | ||||
|                                             "success": True, | ||||
|                                             "data": json.loads(state), | ||||
|                                             "finished_at": datetime.now().isoformat()}) | ||||
|         # Then state object is not in redis | ||||
|         logger.critical("Unable to find user_data in cache.") | ||||
|         return sanic.response.HTTPResponse({"msg": "User state not found", | ||||
|                                             "success": False, | ||||
|                                             "status": 404, | ||||
|                                             "finished_at": datetime.now().isoformat()}, status=404) | ||||
|     except aioredis.ProtocolError: | ||||
|         logger.critical("Unable to connect to state cache") | ||||
|         return sanic.response.HTTPResponse({"msg": "Internal Server Error", | ||||
|                                             "status": 500, | ||||
|                                             "success": False, | ||||
|                                             "finished_at": datetime.now().isoformat()}, status=500) | ||||
|  | ||||
|  | ||||
| @app.route("/state/<user_id>/push", methods=["POST"]) | ||||
| async def set_state(request, user_id): | ||||
|     try: | ||||
|         # Pull a connection from the pool | ||||
|         with await request["redis"] as redis_conn: | ||||
|             # Set the value in cache to your new value | ||||
|             await redis_conn.set(user_id, json.dumps(request.json), expire=1800) | ||||
|             logger.info("Successfully pushed state to cache") | ||||
|             return sanic.response.HTTPResponse({"msg": "Successfully pushed state to cache", | ||||
|                                                 "success": True, | ||||
|                                                 "status": 200, | ||||
|                                                 "finished_at": datetime.now().isoformat()}) | ||||
|     except aioredis.ProtocolError: | ||||
|         logger.critical("Unable to connect to state cache") | ||||
|         return sanic.response.HTTPResponse({"msg": "Internal Server Error", | ||||
|                                             "status": 500, | ||||
|                                             "success": False, | ||||
|                                             "finished_at": datetime.now().isoformat()}, status=500) | ||||
|  | ||||
|  | ||||
| def configure(): | ||||
|     # Setup environment variables | ||||
|     env_vars = [os.environ.get(v, None) for v in ENV_VARS] | ||||
|     if not all(env_vars): | ||||
|         # Send back environment variables that were not set | ||||
|         return False, ", ".join([ENV_VARS[i] for i, flag in env_vars if not flag]) | ||||
|     else: | ||||
|         # Add all the env vars to our app config | ||||
|         app.config.update({k: v for k, v in zip(ENV_VARS, env_vars)}) | ||||
|         setup_logging() | ||||
|     return True, None | ||||
|  | ||||
|  | ||||
| def setup_logging(): | ||||
|     logging_format = "[%(asctime)s] %(process)d-%(levelname)s " | ||||
|     logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " | ||||
|     logging_format += "%(message)s" | ||||
|  | ||||
|     logging.basicConfig( | ||||
|         filename=app.config.APP_LOGFILE, | ||||
|         format=logging_format, | ||||
|         level=logging.DEBUG) | ||||
|  | ||||
|  | ||||
| def main(result, missing): | ||||
|     if result: | ||||
|         try: | ||||
|             app.run(host="0.0.0.0", port=8080, debug=True) | ||||
|         except: | ||||
|             logging.critical("User killed server. Closing") | ||||
|     else: | ||||
|         logging.critical("Unable to start. Missing environment variables [{0}]".format(missing)) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     result, missing = configure() | ||||
|     logger = logging.getLogger() | ||||
|     main(result, missing) | ||||
| @@ -1,22 +1,21 @@ | ||||
| """ | ||||
| Example intercepting uncaught exceptions using Sanic's error handler framework. | ||||
|  | ||||
| This may be useful for developers wishing to use Sentry, Airbrake, etc. | ||||
| or a custom system to log and monitor unexpected errors in production. | ||||
|  | ||||
| First we create our own class inheriting from Handler in sanic.exceptions, | ||||
| and pass in an instance of it when we create our Sanic instance. Inside this | ||||
| class' default handler, we can do anything including sending exceptions to | ||||
| an external service. | ||||
| """ | ||||
| from sanic.exceptions import Handler, SanicException | ||||
| from sanic.handlers import ErrorHandler | ||||
| from sanic.exceptions import SanicException | ||||
| """ | ||||
| Imports and code relevant for our CustomHandler class | ||||
| (Ordinarily this would be in a separate file) | ||||
| """ | ||||
|  | ||||
|  | ||||
| class CustomHandler(Handler): | ||||
| class CustomHandler(ErrorHandler): | ||||
|  | ||||
|     def default(self, request, exception): | ||||
|         # Here, we have access to the exception object | ||||
| @@ -38,11 +37,11 @@ server's error_handler to an instance of our CustomHandler | ||||
| """ | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| from sanic import response | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| handler = CustomHandler(sanic=app) | ||||
| handler = CustomHandler() | ||||
| app.error_handler = handler | ||||
|  | ||||
|  | ||||
| @@ -51,7 +50,7 @@ async def test(request): | ||||
|     # Here, something occurs which causes an unexpected exception | ||||
|     # This exception will flow to our custom handler. | ||||
|     1 / 0 | ||||
|     return json({"test": True}) | ||||
|     return response.json({"test": True}) | ||||
|  | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000, debug=True) | ||||
| @@ -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.response import html | ||||
| from jinja2 import Template | ||||
|  | ||||
| template = Template('Hello {{ name }}!') | ||||
| from sanic import response | ||||
| 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 html(template.render(**data)) | ||||
|     rendered_template = await template.render_async(**data) | ||||
|     return response.html(rendered_template) | ||||
|  | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000) | ||||
| app.run(host="0.0.0.0", port=8080, debug=True) | ||||
							
								
								
									
										26
									
								
								examples/modify_header_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								examples/modify_header_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| """ | ||||
| Modify header or status in response | ||||
| """ | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| @app.route('/') | ||||
| def handle_request(request): | ||||
|     return response.json( | ||||
|         {'message': 'Hello world!'}, | ||||
|         headers={'X-Served-By': 'sanic'}, | ||||
|         status=200 | ||||
|     ) | ||||
|      | ||||
| @app.route('/unauthorized') | ||||
| def handle_request(request): | ||||
|     return response.json( | ||||
|         {'message': 'You are not authorized'}, | ||||
|         headers={'X-Served-By': 'sanic'}, | ||||
|         status=404 | ||||
|     ) | ||||
|  | ||||
| app.run(host="0.0.0.0", port=8000, debug=True) | ||||
| @@ -1,6 +1,5 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import text | ||||
| import json | ||||
| from sanic import response | ||||
| import logging | ||||
|  | ||||
| logging_format = "[%(asctime)s] %(process)d-%(levelname)s " | ||||
| @@ -18,6 +17,6 @@ sanic = Sanic() | ||||
| @sanic.route("/") | ||||
| def test(request): | ||||
|     log.info("received request; responding with 'hey'") | ||||
|     return text("hey") | ||||
|     return response.text("hey") | ||||
|  | ||||
| sanic.run(host="0.0.0.0", port=8000) | ||||
|   | ||||
							
								
								
									
										85
									
								
								examples/plotly_example/plotlyjs_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								examples/plotly_example/plotlyjs_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| from sanic import Sanic | ||||
|  | ||||
| from sanic_session import InMemorySessionInterface | ||||
| from sanic_jinja2 import SanicJinja2 | ||||
|  | ||||
| import json | ||||
| import plotly | ||||
|  | ||||
| import pandas as pd | ||||
| import numpy as np | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| jinja = SanicJinja2(app) | ||||
| session = InMemorySessionInterface(cookie_name=app.name, prefix=app.name) | ||||
|  | ||||
| @app.middleware('request') | ||||
| async def print_on_request(request): | ||||
| 	print(request.headers) | ||||
| 	await session.open(request) | ||||
|  | ||||
| @app.middleware('response') | ||||
| async def print_on_response(request, response): | ||||
| 	await session.save(request, response) | ||||
|  | ||||
|  | ||||
|  | ||||
| @app.route('/') | ||||
| async def index(request): | ||||
|     rng = pd.date_range('1/1/2011', periods=7500, freq='H') | ||||
|     ts = pd.Series(np.random.randn(len(rng)), index=rng) | ||||
|  | ||||
|     graphs = [ | ||||
|         dict( | ||||
|             data=[ | ||||
|                 dict( | ||||
|                     x=[1, 2, 3], | ||||
|                     y=[10, 20, 30], | ||||
|                     type='scatter' | ||||
|                 ), | ||||
|             ], | ||||
|             layout=dict( | ||||
|                 title='first graph' | ||||
|             ) | ||||
|         ), | ||||
|  | ||||
|         dict( | ||||
|             data=[ | ||||
|                 dict( | ||||
|                     x=[1, 3, 5], | ||||
|                     y=[10, 50, 30], | ||||
|                     type='bar' | ||||
|                 ), | ||||
|             ], | ||||
|             layout=dict( | ||||
|                 title='second graph' | ||||
|             ) | ||||
|         ), | ||||
|  | ||||
|         dict( | ||||
|             data=[ | ||||
|                 dict( | ||||
|                     x=ts.index,  # Can use the pandas data structures directly | ||||
|                     y=ts | ||||
|                 ) | ||||
|             ] | ||||
|         ) | ||||
|     ] | ||||
|  | ||||
|     # Add "ids" to each of the graphs to pass up to the client | ||||
|     # for templating | ||||
|     ids = ['graph-{}'.format(i) for i, _ in enumerate(graphs)] | ||||
|  | ||||
|     # Convert the figures to JSON | ||||
|     # PlotlyJSONEncoder appropriately converts pandas, datetime, etc | ||||
|     # objects to their JSON equivalents | ||||
|     graphJSON = json.dumps(graphs, cls=plotly.utils.PlotlyJSONEncoder) | ||||
|  | ||||
|     return jinja.render('index.html', request, | ||||
|                            ids=ids, | ||||
|                            graphJSON=graphJSON) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host='0.0.0.0', port=8000, debug=True) | ||||
							
								
								
									
										5
									
								
								examples/plotly_example/requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								examples/plotly_example/requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| pandas==0.19.2 | ||||
| plotly==2.0.7 | ||||
| sanic==0.5.0 | ||||
| sanic-jinja2==0.5.1 | ||||
| sanic-session==0.1.3 | ||||
							
								
								
									
										0
									
								
								examples/plotly_example/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								examples/plotly_example/templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								examples/redirect_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								examples/redirect_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
|      | ||||
| @app.route('/') | ||||
| def handle_request(request): | ||||
|     return response.redirect('/redirect') | ||||
|      | ||||
| @app.route('/redirect') | ||||
| async def test(request): | ||||
|     return response.json({"Redirected": True}) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="0.0.0.0", port=8000) | ||||
| @@ -1,6 +1,6 @@ | ||||
| from sanic import Sanic | ||||
| import asyncio | ||||
| from sanic.response import text | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
| from sanic.config import Config | ||||
| from sanic.exceptions import RequestTimeout | ||||
|  | ||||
| @@ -11,11 +11,11 @@ app = Sanic(__name__) | ||||
| @app.route('/') | ||||
| async def test(request): | ||||
|     await asyncio.sleep(3) | ||||
|     return text('Hello, world!') | ||||
|     return response.text('Hello, world!') | ||||
|  | ||||
|  | ||||
| @app.exception(RequestTimeout) | ||||
| def timeout(request, exception): | ||||
|     return text('RequestTimeout from error_handler.', 408) | ||||
|     return response.text('RequestTimeout from error_handler.', 408) | ||||
|  | ||||
| app.run(host='0.0.0.0', port=8000) | ||||
| @@ -1,5 +1,5 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| from sanic import response | ||||
| from multiprocessing import Event | ||||
| from signal import signal, SIGINT | ||||
| import asyncio | ||||
| @@ -9,10 +9,10 @@ app = Sanic(__name__) | ||||
|  | ||||
| @app.route("/") | ||||
| async def test(request): | ||||
|     return json({"answer": "42"}) | ||||
|     return response.json({"answer": "42"}) | ||||
|  | ||||
| asyncio.set_event_loop(uvloop.new_event_loop()) | ||||
| server = app.create_server(host="0.0.0.0", port=8001) | ||||
| server = app.create_server(host="0.0.0.0", port=8000) | ||||
| loop = asyncio.get_event_loop() | ||||
| task = asyncio.ensure_future(server) | ||||
| signal(SIGINT, lambda s, f: loop.stop()) | ||||
|   | ||||
							
								
								
									
										62
									
								
								examples/sanic_aiomysql_with_global_pool.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								examples/sanic_aiomysql_with_global_pool.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| # encoding: utf-8 | ||||
| """ | ||||
| You need the aiomysql | ||||
| """ | ||||
| import os | ||||
|  | ||||
| import aiomysql | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
|  | ||||
| database_name = os.environ['DATABASE_NAME'] | ||||
| database_host = os.environ['DATABASE_HOST'] | ||||
| database_user = os.environ['DATABASE_USER'] | ||||
| database_password = os.environ['DATABASE_PASSWORD'] | ||||
| app = Sanic() | ||||
|  | ||||
|  | ||||
| @app.listener("before_server_start") | ||||
| async def get_pool(app, loop): | ||||
|     """ | ||||
|     the first param  is the global instance , | ||||
|     so we can store our connection pool in it . | ||||
|     and it can be used by different request | ||||
|     :param args: | ||||
|     :param kwargs: | ||||
|     :return: | ||||
|     """ | ||||
|     app.pool = { | ||||
|         "aiomysql": await aiomysql.create_pool(host=database_host, user=database_user, password=database_password, | ||||
|                                                db=database_name, | ||||
|                                                maxsize=5)} | ||||
|     async with app.pool['aiomysql'].acquire() as conn: | ||||
|         async with conn.cursor() as cur: | ||||
|             await cur.execute('DROP TABLE IF EXISTS sanic_polls') | ||||
|             await cur.execute("""CREATE TABLE sanic_polls ( | ||||
|                                     id serial primary key, | ||||
|                                     question varchar(50), | ||||
|                                     pub_date timestamp | ||||
|                                 );""") | ||||
|             for i in range(0, 100): | ||||
|                 await cur.execute("""INSERT INTO sanic_polls | ||||
|                                 (id, question, pub_date) VALUES ({}, {}, now()) | ||||
|                 """.format(i, i)) | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| async def test(): | ||||
|     result = [] | ||||
|     data = {} | ||||
|     async with app.pool['aiomysql'].acquire() as conn: | ||||
|         async with conn.cursor() as cur: | ||||
|             await cur.execute("SELECT question, pub_date FROM sanic_polls") | ||||
|             async for row in cur: | ||||
|                 result.append({"question": row[0], "pub_date": row[1]}) | ||||
|     if result or len(result) > 0: | ||||
|         data['data'] = res | ||||
|     return json(data) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="127.0.0.1", workers=4, port=12000) | ||||
							
								
								
									
										34
									
								
								examples/sanic_aioredis_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								examples/sanic_aioredis_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| """ To run this example you need additional aioredis package | ||||
| """ | ||||
| from sanic import Sanic, response | ||||
| import aioredis | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| async def handle(request): | ||||
|     async with request.app.redis_pool.get() as redis: | ||||
|         await redis.set('test-my-key', 'value') | ||||
|         val = await redis.get('test-my-key') | ||||
|     return response.text(val.decode('utf-8')) | ||||
|  | ||||
|  | ||||
| @app.listener('before_server_start') | ||||
| async def before_server_start(app, loop): | ||||
|     app.redis_pool = await aioredis.create_pool( | ||||
|         ('localhost', 6379), | ||||
|         minsize=5, | ||||
|         maxsize=10, | ||||
|         loop=loop | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @app.listener('after_server_stop') | ||||
| async def after_server_stop(app, loop): | ||||
|     app.redis_pool.close() | ||||
|     await app.redis_pool.wait_closed() | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="0.0.0.0", port=8000) | ||||
| @@ -1,59 +1,51 @@ | ||||
| """ To run this example you need additional asyncpg package | ||||
|  | ||||
| """ | ||||
| import os | ||||
| import asyncio | ||||
|  | ||||
| import uvloop | ||||
| from asyncpg import create_pool | ||||
| from asyncpg import connect, create_pool | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
|  | ||||
| DB_CONFIG = { | ||||
|     'host': '<host>', | ||||
|     'user': '<username>', | ||||
|     'user': '<user>', | ||||
|     'password': '<password>', | ||||
|     'port': '<port>', | ||||
|     'database': '<database>' | ||||
| } | ||||
|  | ||||
|  | ||||
| def jsonify(records): | ||||
|     """ | ||||
|     Parse asyncpg record response into JSON format | ||||
|     """ | ||||
|     return [{key: value for key, value in | ||||
|              zip(r.keys(), r.values())} for r in records] | ||||
|     return [dict(r.items()) for r in records] | ||||
|  | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
|  | ||||
| @app.listener('before_server_start') | ||||
| async def create_db(app, loop): | ||||
|     """ | ||||
|     Create some table and add some data | ||||
|     """ | ||||
|     async with create_pool(**DB_CONFIG) as pool: | ||||
|         async with pool.acquire() as connection: | ||||
|             async with connection.transaction(): | ||||
| async def register_db(app, loop): | ||||
|     app.pool = await create_pool(**DB_CONFIG, loop=loop, max_size=100) | ||||
|     async with app.pool.acquire() as connection: | ||||
|         await connection.execute('DROP TABLE IF EXISTS sanic_post') | ||||
|         await connection.execute("""CREATE TABLE sanic_post ( | ||||
|                                 id serial primary key, | ||||
|                                 content varchar(50), | ||||
|                                 post_date timestamp | ||||
|                             );""") | ||||
|                 for i in range(0, 100): | ||||
|         for i in range(0, 1000): | ||||
|             await connection.execute(f"""INSERT INTO sanic_post | ||||
|                 (id, content, post_date) VALUES ({i}, {i}, now())""") | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| async def handler(request): | ||||
|     async with create_pool(**DB_CONFIG) as pool: | ||||
|         async with pool.acquire() as connection: | ||||
|             async with connection.transaction(): | ||||
| @app.get('/') | ||||
| async def root_get(request): | ||||
|     async with app.pool.acquire() as connection: | ||||
|         results = await connection.fetch('SELECT * FROM sanic_post') | ||||
|         return json({'posts': jsonify(results)}) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host='0.0.0.0', port=8000) | ||||
|     app.run(host='127.0.0.1', port=8080) | ||||
|   | ||||
| @@ -5,7 +5,7 @@ motor==1.1 | ||||
| sanic==0.2.0 | ||||
| """ | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| from sanic import response | ||||
|  | ||||
|  | ||||
| app = Sanic('motor_mongodb') | ||||
| @@ -25,7 +25,7 @@ async def get(request): | ||||
|     for doc in docs: | ||||
|         doc['id'] = str(doc['_id']) | ||||
|         del doc['_id'] | ||||
|     return json(docs) | ||||
|     return response.json(docs) | ||||
|  | ||||
|  | ||||
| @app.route('/post', methods=['POST']) | ||||
| @@ -34,8 +34,8 @@ async def new(request): | ||||
|     print(doc) | ||||
|     db = get_db() | ||||
|     object_id = await db.test_col.save(doc) | ||||
|     return json({'object_id': str(object_id)}) | ||||
|     return response.json({'object_id': str(object_id)}) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     app.run(host='127.0.0.1', port=8000) | ||||
|     app.run(host='0.0.0.0', port=8000, debug=True) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
|  | ||||
| ## You need the following additional packages for this example | ||||
| # aiopg | ||||
| # peewee_async | ||||
| @@ -10,8 +11,9 @@ from sanic.response import json | ||||
|  | ||||
| ## peewee_async related imports | ||||
| import peewee | ||||
| from peewee_async import Manager, PostgresqlDatabase | ||||
|  | ||||
| from peewee import Model, BaseModel | ||||
| from peewee_async import Manager, PostgresqlDatabase, execute | ||||
| from functools import partial | ||||
|  # we instantiate a custom loop so we can pass it to our db manager | ||||
|  | ||||
| ## from peewee_async docs: | ||||
| @@ -19,42 +21,77 @@ from peewee_async import Manager, PostgresqlDatabase | ||||
| # with manager! It’s all automatic. But you can run Manager.connect() or | ||||
| # Manager.close() when you need it. | ||||
|  | ||||
| class AsyncManager(Manager): | ||||
|     """Inherit the peewee_async manager with our own object | ||||
|        configuration | ||||
|  | ||||
| # let's create a simple key value store: | ||||
| class KeyValue(peewee.Model): | ||||
|     key = peewee.CharField(max_length=40, unique=True) | ||||
|     text = peewee.TextField(default='') | ||||
|        database.allow_sync = False | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, _model_class, *args, **kwargs): | ||||
|         super(AsyncManager, self).__init__(*args, **kwargs) | ||||
|         self._model_class = _model_class | ||||
|         self.database.allow_sync = False | ||||
|  | ||||
|     def _do_fill(self, method, *args, **kwargs): | ||||
|         _class_method = getattr(super(AsyncManager, self), method) | ||||
|         pf = partial(_class_method, self._model_class) | ||||
|         return pf(*args, **kwargs) | ||||
|  | ||||
|     def new(self, *args, **kwargs): | ||||
|         return self._do_fill('create', *args, **kwargs) | ||||
|  | ||||
|     def get(self, *args, **kwargs): | ||||
|         return self._do_fill('get', *args, **kwargs) | ||||
|  | ||||
|     def execute(self, query): | ||||
|         return execute(query) | ||||
|  | ||||
|  | ||||
| def _get_meta_db_class(db): | ||||
|     """creating a declartive class model for db""" | ||||
|     class _BlockedMeta(BaseModel): | ||||
|         def __new__(cls, name, bases, attrs): | ||||
|             _instance = super(_BlockedMeta, cls).__new__(cls, name, bases, attrs) | ||||
|             _instance.objects = AsyncManager(_instance, db) | ||||
|             return _instance | ||||
|  | ||||
|     class _Base(Model, metaclass=_BlockedMeta): | ||||
|  | ||||
|         def to_dict(self): | ||||
|             return self._data | ||||
|  | ||||
|         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 | ||||
|             database=db | ||||
|     return _Base | ||||
|  | ||||
|  | ||||
| app = Sanic('peewee_example') | ||||
| def declarative_base(*args, **kwargs): | ||||
|     """Returns a new Modeled Class after inheriting meta and Model classes""" | ||||
|     db = PostgresqlDatabase(*args, **kwargs) | ||||
|     return _get_meta_db_class(db) | ||||
|  | ||||
| @app.listener('before_server_start') | ||||
| def setup(app, loop): | ||||
|     database = PostgresqlDatabase(database='test', | ||||
|  | ||||
| AsyncBaseModel = declarative_base(database='test', | ||||
|                                   host='127.0.0.1', | ||||
|                                   user='postgres', | ||||
|                                   password='mysecretpassword') | ||||
|  | ||||
|     objects = Manager(database, loop=loop) | ||||
| # let's create a simple key value store: | ||||
| class KeyValue(AsyncBaseModel): | ||||
|     key = peewee.CharField(max_length=40, unique=True) | ||||
|     text = peewee.TextField(default='') | ||||
|  | ||||
|  | ||||
| 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) | ||||
|     obj = await KeyValue.objects.new(key=key, text=value) | ||||
|     return json({'object_id': obj.id}) | ||||
|  | ||||
|  | ||||
| @@ -63,7 +100,7 @@ async def get(request): | ||||
|     """ | ||||
|     Load all objects from database | ||||
|     """ | ||||
|     all_objects = await objects.execute(KeyValue.select()) | ||||
|     all_objects = await KeyValue.objects.execute(KeyValue.select()) | ||||
|     serialized_obj = [] | ||||
|     for obj in all_objects: | ||||
|         serialized_obj.append({ | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import json | ||||
| from sanic import response | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
|  | ||||
| @app.route("/") | ||||
| async def test(request): | ||||
|     return json({"test": True}) | ||||
|     return response.json({"test": True}) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="0.0.0.0", port=8000) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import os | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.log import log | ||||
| from sanic.response import json, text, file | ||||
| from sanic import response | ||||
| from sanic.exceptions import ServerError | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| @@ -10,17 +10,17 @@ app = Sanic(__name__) | ||||
|  | ||||
| @app.route("/") | ||||
| async def test_async(request): | ||||
|     return json({"test": True}) | ||||
|     return response.json({"test": True}) | ||||
|  | ||||
|  | ||||
| @app.route("/sync", methods=['GET', 'POST']) | ||||
| def test_sync(request): | ||||
|     return json({"test": True}) | ||||
|     return response.json({"test": True}) | ||||
|  | ||||
|  | ||||
| @app.route("/dynamic/<name>/<id:int>") | ||||
| def test_params(request, name, id): | ||||
|     return text("yeehaww {} {}".format(name, id)) | ||||
|     return response.text("yeehaww {} {}".format(name, id)) | ||||
|  | ||||
|  | ||||
| @app.route("/exception") | ||||
| @@ -31,11 +31,11 @@ def exception(request): | ||||
| async def test_await(request): | ||||
|     import asyncio | ||||
|     await asyncio.sleep(5) | ||||
|     return text("I'm feeling sleepy") | ||||
|     return response.text("I'm feeling sleepy") | ||||
|  | ||||
| @app.route("/file") | ||||
| async def test_file(request): | ||||
|     return await file(os.path.abspath("setup.py")) | ||||
|     return await response.file(os.path.abspath("setup.py")) | ||||
|  | ||||
|  | ||||
| # ----------------------------------------------- # | ||||
| @@ -44,7 +44,7 @@ async def test_file(request): | ||||
|  | ||||
| @app.exception(ServerError) | ||||
| async def test(request, exception): | ||||
|     return json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code) | ||||
|     return response.json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code) | ||||
|  | ||||
|  | ||||
| # ----------------------------------------------- # | ||||
| @@ -53,23 +53,28 @@ async def test(request, exception): | ||||
|  | ||||
| @app.route("/json") | ||||
| def post_json(request): | ||||
|     return json({"received": True, "message": request.json}) | ||||
|     return response.json({"received": True, "message": request.json}) | ||||
|  | ||||
|  | ||||
| @app.route("/form") | ||||
| def post_json(request): | ||||
|     return json({"received": True, "form_data": request.form, "test": request.form.get('test')}) | ||||
|     return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')}) | ||||
|  | ||||
|  | ||||
| @app.route("/query_string") | ||||
| def query_string(request): | ||||
|     return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string}) | ||||
|     return response.json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string}) | ||||
|  | ||||
|  | ||||
| # ----------------------------------------------- # | ||||
| # Run Server | ||||
| # ----------------------------------------------- # | ||||
|  | ||||
| @app.listener('before_server_start') | ||||
| def before_start(app, loop): | ||||
|     log.info("SERVER STARTING") | ||||
|  | ||||
|  | ||||
| @app.listener('after_server_start') | ||||
| def after_start(app, loop): | ||||
|     log.info("OH OH OH OH OHHHHHHHH") | ||||
| @@ -77,7 +82,13 @@ def after_start(app, loop): | ||||
|  | ||||
| @app.listener('before_server_stop') | ||||
| def before_stop(app, loop): | ||||
|     log.info("SERVER STOPPING") | ||||
|  | ||||
|  | ||||
| @app.listener('after_server_stop') | ||||
| def after_stop(app, loop): | ||||
|     log.info("TRIED EVERYTHING") | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="0.0.0.0", port=8000, debug=True) | ||||
|   | ||||
							
								
								
									
										18
									
								
								examples/url_for_example.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								examples/url_for_example.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| from sanic import Sanic | ||||
| from sanic import response | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
| @app.route('/') | ||||
| async def index(request): | ||||
|     # generate a URL for the endpoint `post_handler` | ||||
|     url = app.url_for('post_handler', post_id=5) | ||||
|     # the URL is `/posts/5`, redirect to it | ||||
|     return response.redirect(url) | ||||
|  | ||||
| @app.route('/posts/<post_id>') | ||||
| async def post_handler(request, post_id): | ||||
|     return response.text('Post - {}'.format(post_id)) | ||||
|      | ||||
| if __name__ == '__main__': | ||||
|     app.run(host="0.0.0.0", port=8000, debug=True) | ||||
| @@ -1,4 +1,4 @@ | ||||
| from sanic.response import text | ||||
| from sanic import response | ||||
| from sanic import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
|  | ||||
| @@ -15,23 +15,23 @@ bp = Blueprint("bp", host="bp.example.com") | ||||
|                       "somethingelse.com", | ||||
|                       "therestofyourdomains.com"]) | ||||
| async def hello(request): | ||||
|     return text("Some defaults") | ||||
|     return response.text("Some defaults") | ||||
|  | ||||
| @app.route('/', host="example.com") | ||||
| async def hello(request): | ||||
|     return text("Answer") | ||||
|     return response.text("Answer") | ||||
|  | ||||
| @app.route('/', host="sub.example.com") | ||||
| async def hello(request): | ||||
|     return text("42") | ||||
|     return response.text("42") | ||||
|  | ||||
| @bp.route("/question") | ||||
| async def hello(request): | ||||
|     return text("What is the meaning of life?") | ||||
|     return response.text("What is the meaning of life?") | ||||
|  | ||||
| @bp.route("/answer") | ||||
| async def hello(request): | ||||
|     return text("42") | ||||
|     return response.text("42") | ||||
|  | ||||
| app.register_blueprint(bp) | ||||
|  | ||||
|   | ||||
							
								
								
									
										29
									
								
								examples/websocket.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								examples/websocket.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| <!DOCTYPE html> | ||||
| <html> | ||||
|     <head> | ||||
|         <title>WebSocket demo</title> | ||||
|     </head> | ||||
|     <body> | ||||
|         <script> | ||||
|             var ws = new WebSocket('ws://' + document.domain + ':' + location.port + '/feed'), | ||||
|                 messages = document.createElement('ul'); | ||||
|             ws.onmessage = function (event) { | ||||
|                 var messages = document.getElementsByTagName('ul')[0], | ||||
|                     message = document.createElement('li'), | ||||
|                     content = document.createTextNode('Received: ' + event.data); | ||||
|                 message.appendChild(content); | ||||
|                 messages.appendChild(message); | ||||
|             }; | ||||
|             document.body.appendChild(messages); | ||||
|             window.setInterval(function() { | ||||
|                 data = 'bye!' | ||||
|                 ws.send(data); | ||||
|                 var messages = document.getElementsByTagName('ul')[0], | ||||
|                     message = document.createElement('li'), | ||||
|                     content = document.createTextNode('Sent: ' + data); | ||||
|                 message.appendChild(content); | ||||
|                 messages.appendChild(message); | ||||
|             }, 1000); | ||||
|         </script> | ||||
|     </body> | ||||
| </html> | ||||
							
								
								
									
										23
									
								
								examples/websocket.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								examples/websocket.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import file | ||||
|  | ||||
| app = Sanic(__name__) | ||||
|  | ||||
|  | ||||
| @app.route('/') | ||||
| async def index(request): | ||||
|     return await file('websocket.html') | ||||
|  | ||||
|  | ||||
| @app.websocket('/feed') | ||||
| async def feed(request, ws): | ||||
|     while True: | ||||
|         data = 'hello!' | ||||
|         print('Sending: ' + data) | ||||
|         await ws.send(data) | ||||
|         data = await ws.recv() | ||||
|         print('Received: ' + data) | ||||
|  | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|     app.run() | ||||
| @@ -1,18 +1,11 @@ | ||||
| aiocache | ||||
| aiofiles | ||||
| aiohttp | ||||
| aiohttp==1.3.5 | ||||
| chardet<=2.3.0 | ||||
| beautifulsoup4 | ||||
| bottle | ||||
| coverage | ||||
| falcon | ||||
| gunicorn | ||||
| httptools | ||||
| kyoukai | ||||
| flake8 | ||||
| pytest | ||||
| recommonmark | ||||
| sphinx | ||||
| sphinx_rtd_theme | ||||
| tornado | ||||
| tox | ||||
| ujson | ||||
| uvloop | ||||
|   | ||||
| @@ -2,3 +2,4 @@ aiofiles | ||||
| httptools | ||||
| ujson | ||||
| uvloop | ||||
| websockets | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| from sanic.app import Sanic | ||||
| from sanic.blueprints import Blueprint | ||||
|  | ||||
| __version__ = '0.4.1' | ||||
| __version__ = '0.5.2' | ||||
|  | ||||
| __all__ = ['Sanic', 'Blueprint'] | ||||
|   | ||||
| @@ -8,6 +8,10 @@ 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('--cert', dest='cert', type=str, | ||||
|                         help='location of certificate for SSL') | ||||
|     parser.add_argument('--key', dest='key', type=str, | ||||
|                         help='location of keyfile for SSL.') | ||||
|     parser.add_argument('--workers', dest='workers', type=int, default=1, ) | ||||
|     parser.add_argument('--debug', dest='debug', action="store_true") | ||||
|     parser.add_argument('module') | ||||
| @@ -24,9 +28,13 @@ if __name__ == "__main__": | ||||
|             raise ValueError("Module is not a Sanic app, it is a {}.  " | ||||
|                              "Perhaps you meant {}.app?" | ||||
|                              .format(type(app).__name__, args.module)) | ||||
|         if args.cert is not None or args.key is not None: | ||||
|             ssl = {'cert': args.cert, 'key': args.key} | ||||
|         else: | ||||
|             ssl = None | ||||
|  | ||||
|         app.run(host=args.host, port=args.port, | ||||
|                 workers=args.workers, debug=args.debug) | ||||
|                 workers=args.workers, debug=args.debug, ssl=ssl) | ||||
|     except ImportError: | ||||
|         log.error("No module named {} found.\n" | ||||
|                   "  Example File: project/sanic_server.py -> app\n" | ||||
|   | ||||
							
								
								
									
										261
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										261
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -1,29 +1,32 @@ | ||||
| import logging | ||||
| import re | ||||
| import warnings | ||||
| from asyncio import get_event_loop | ||||
| from asyncio import get_event_loop, ensure_future, CancelledError | ||||
| from collections import deque, defaultdict | ||||
| from functools import partial | ||||
| from inspect import isawaitable, stack, getmodulename | ||||
| from traceback import format_exc | ||||
| from urllib.parse import urlencode, urlunparse | ||||
| from ssl import create_default_context, Purpose | ||||
|  | ||||
| from sanic.config import Config | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.exceptions import ServerError, URLBuildError, SanicException | ||||
| from sanic.handlers import ErrorHandler | ||||
| from sanic.log import log | ||||
| from sanic.response import HTTPResponse | ||||
| 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 TestClient | ||||
| from sanic.testing import SanicTestClient | ||||
| from sanic.views import CompositionView | ||||
| from sanic.websocket import WebSocketProtocol, ConnectionClosed | ||||
|  | ||||
|  | ||||
| class Sanic: | ||||
|  | ||||
|     def __init__(self, name=None, router=None, error_handler=None): | ||||
|     def __init__(self, name=None, router=None, error_handler=None, | ||||
|                  load_env=True, request_class=None): | ||||
|         # Only set up a default log handler if the | ||||
|         # end-user application didn't set anything up. | ||||
|         if not logging.root.handlers and log.level == logging.NOTSET: | ||||
| @@ -41,8 +44,9 @@ class Sanic: | ||||
|  | ||||
|         self.name = name | ||||
|         self.router = router or Router() | ||||
|         self.request_class = request_class | ||||
|         self.error_handler = error_handler or ErrorHandler() | ||||
|         self.config = Config() | ||||
|         self.config = Config(load_env=load_env) | ||||
|         self.request_middleware = deque() | ||||
|         self.response_middleware = deque() | ||||
|         self.blueprints = {} | ||||
| @@ -51,6 +55,8 @@ class Sanic: | ||||
|         self.sock = None | ||||
|         self.listeners = defaultdict(list) | ||||
|         self.is_running = False | ||||
|         self.websocket_enabled = False | ||||
|         self.websocket_tasks = [] | ||||
|  | ||||
|         # Register alternative method names | ||||
|         self.go_fast = self.run | ||||
| @@ -98,7 +104,8 @@ class Sanic: | ||||
|         return decorator | ||||
|  | ||||
|     # Decorator | ||||
|     def route(self, uri, methods=frozenset({'GET'}), host=None): | ||||
|     def route(self, uri, methods=frozenset({'GET'}), host=None, | ||||
|               strict_slashes=False): | ||||
|         """Decorate a function to be registered as a route | ||||
|  | ||||
|         :param uri: path of the URL | ||||
| @@ -114,34 +121,42 @@ class Sanic: | ||||
|  | ||||
|         def response(handler): | ||||
|             self.router.add(uri=uri, methods=methods, handler=handler, | ||||
|                             host=host) | ||||
|                             host=host, strict_slashes=strict_slashes) | ||||
|             return handler | ||||
|  | ||||
|         return response | ||||
|  | ||||
|     # Shorthand method decorators | ||||
|     def get(self, uri, host=None): | ||||
|         return self.route(uri, methods=frozenset({"GET"}), host=host) | ||||
|     def get(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=frozenset({"GET"}), host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def post(self, uri, host=None): | ||||
|         return self.route(uri, methods=frozenset({"POST"}), host=host) | ||||
|     def post(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=frozenset({"POST"}), host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def put(self, uri, host=None): | ||||
|         return self.route(uri, methods=frozenset({"PUT"}), host=host) | ||||
|     def put(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=frozenset({"PUT"}), host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def head(self, uri, host=None): | ||||
|         return self.route(uri, methods=frozenset({"HEAD"}), host=host) | ||||
|     def head(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=frozenset({"HEAD"}), host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def options(self, uri, host=None): | ||||
|         return self.route(uri, methods=frozenset({"OPTIONS"}), host=host) | ||||
|     def options(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def patch(self, uri, host=None): | ||||
|         return self.route(uri, methods=frozenset({"PATCH"}), host=host) | ||||
|     def patch(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=frozenset({"PATCH"}), host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def delete(self, uri, host=None): | ||||
|         return self.route(uri, methods=frozenset({"DELETE"}), host=host) | ||||
|     def delete(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=frozenset({"DELETE"}), host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): | ||||
|     def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, | ||||
|                   strict_slashes=False): | ||||
|         """A helper method to register class instance or | ||||
|         functions as a handler to the application url | ||||
|         routes. | ||||
| @@ -165,9 +180,71 @@ class Sanic: | ||||
|         if isinstance(handler, CompositionView): | ||||
|             methods = handler.handlers.keys() | ||||
|  | ||||
|         self.route(uri=uri, methods=methods, host=host)(handler) | ||||
|         self.route(uri=uri, methods=methods, host=host, | ||||
|                    strict_slashes=strict_slashes)(handler) | ||||
|         return handler | ||||
|  | ||||
|     # Decorator | ||||
|     def websocket(self, uri, host=None, strict_slashes=False): | ||||
|         """Decorate a function to be registered as a websocket route | ||||
|         :param uri: path of the URL | ||||
|         :param host: | ||||
|         :return: decorated function | ||||
|         """ | ||||
|         self.enable_websocket() | ||||
|  | ||||
|         # 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): | ||||
|             async def websocket_handler(request, *args, **kwargs): | ||||
|                 request.app = self | ||||
|                 protocol = request.transport.get_protocol() | ||||
|                 ws = await protocol.websocket_handshake(request) | ||||
|  | ||||
|                 # schedule the application handler | ||||
|                 # its future is kept in self.websocket_tasks in case it | ||||
|                 # needs to be cancelled due to the server being stopped | ||||
|                 fut = ensure_future(handler(request, ws, *args, **kwargs)) | ||||
|                 self.websocket_tasks.append(fut) | ||||
|                 try: | ||||
|                     await fut | ||||
|                 except (CancelledError, ConnectionClosed): | ||||
|                     pass | ||||
|                 self.websocket_tasks.remove(fut) | ||||
|                 await ws.close() | ||||
|  | ||||
|             self.router.add(uri=uri, handler=websocket_handler, | ||||
|                             methods=frozenset({'GET'}), host=host, | ||||
|                             strict_slashes=strict_slashes) | ||||
|             return handler | ||||
|  | ||||
|         return response | ||||
|  | ||||
|     def add_websocket_route(self, handler, uri, host=None, | ||||
|                             strict_slashes=False): | ||||
|         """A helper method to register a function as a websocket route.""" | ||||
|         return self.websocket(uri, host=host, | ||||
|                               strict_slashes=strict_slashes)(handler) | ||||
|  | ||||
|     def enable_websocket(self, enable=True): | ||||
|         """Enable or disable the support for websocket. | ||||
|  | ||||
|         Websocket is enabled automatically if websocket routes are | ||||
|         added to the application. | ||||
|         """ | ||||
|         if not self.websocket_enabled: | ||||
|             # if the server is stopped, we want to cancel any ongoing | ||||
|             # websocket tasks, to allow the server to exit promptly | ||||
|             @self.listener('before_server_stop') | ||||
|             def cancel_websocket_tasks(app, loop): | ||||
|                 for task in self.websocket_tasks: | ||||
|                     task.cancel() | ||||
|  | ||||
|         self.websocket_enabled = enable | ||||
|  | ||||
|     def remove_route(self, uri, clean_cache=True, host=None): | ||||
|         self.router.remove(uri, clean_cache, host) | ||||
|  | ||||
| @@ -181,6 +258,10 @@ class Sanic: | ||||
|  | ||||
|         def response(handler): | ||||
|             for exception in exceptions: | ||||
|                 if isinstance(exception, (tuple, list)): | ||||
|                     for e in exception: | ||||
|                         self.error_handler.add(e, handler) | ||||
|                 else: | ||||
|                     self.error_handler.add(exception, handler) | ||||
|             return handler | ||||
|  | ||||
| @@ -207,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 | ||||
| @@ -254,7 +335,7 @@ class Sanic: | ||||
|         the output URL's query string. | ||||
|  | ||||
|         :param view_name: string referencing the view name | ||||
|         :param **kwargs: keys and values that are used to build request | ||||
|         :param \*\*kwargs: keys and values that are used to build request | ||||
|             parameters and query string arguments. | ||||
|  | ||||
|         :return: the built URL | ||||
| @@ -345,14 +426,17 @@ class Sanic: | ||||
|     def converted_response_type(self, response): | ||||
|         pass | ||||
|  | ||||
|     async def handle_request(self, request, response_callback): | ||||
|     async def handle_request(self, request, write_callback, stream_callback): | ||||
|         """Take a request from the HTTP Server and return a response object | ||||
|         to be sent back The HTTP Server only expects a response object, so | ||||
|         exception handling must be done here | ||||
|  | ||||
|         :param request: HTTP Request object | ||||
|         :param response_callback: Response function to be called with the | ||||
|                                   response as the only argument | ||||
|         :param write_callback: Synchronous response function to be | ||||
|             called with the response as the only argument | ||||
|         :param stream_callback: Coroutine that handles streaming a | ||||
|             StreamingHTTPResponse if produced by the handler. | ||||
|  | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         try: | ||||
| @@ -361,17 +445,7 @@ class Sanic: | ||||
|             # -------------------------------------------- # | ||||
|  | ||||
|             request.app = self | ||||
|  | ||||
|             response = False | ||||
|             # The if improves speed.  I don't know why | ||||
|             if self.request_middleware: | ||||
|                 for middleware in self.request_middleware: | ||||
|                     response = middleware(request) | ||||
|                     if isawaitable(response): | ||||
|                         response = await response | ||||
|                     if response: | ||||
|                         break | ||||
|  | ||||
|             response = await self._run_request_middleware(request) | ||||
|             # No middleware results | ||||
|             if not response: | ||||
|                 # -------------------------------------------- # | ||||
| @@ -389,20 +463,6 @@ class Sanic: | ||||
|                 response = handler(request, *args, **kwargs) | ||||
|                 if isawaitable(response): | ||||
|                     response = await response | ||||
|  | ||||
|             # -------------------------------------------- # | ||||
|             # Response Middleware | ||||
|             # -------------------------------------------- # | ||||
|  | ||||
|             if self.response_middleware: | ||||
|                 for middleware in self.response_middleware: | ||||
|                     _response = middleware(request, response) | ||||
|                     if isawaitable(_response): | ||||
|                         _response = await _response | ||||
|                     if _response: | ||||
|                         response = _response | ||||
|                         break | ||||
|  | ||||
|         except Exception as e: | ||||
|             # -------------------------------------------- # | ||||
|             # Response Generation Failed | ||||
| @@ -420,8 +480,23 @@ class Sanic: | ||||
|                 else: | ||||
|                     response = HTTPResponse( | ||||
|                         "An error occurred while handling an error") | ||||
|         finally: | ||||
|             # -------------------------------------------- # | ||||
|             # Response Middleware | ||||
|             # -------------------------------------------- # | ||||
|             try: | ||||
|                 response = await self._run_response_middleware(request, | ||||
|                                                                response) | ||||
|             except: | ||||
|                 log.exception( | ||||
|                     'Exception occured in one of response middleware handlers' | ||||
|                 ) | ||||
|  | ||||
|         response_callback(response) | ||||
|         # pass the response to the correct callback | ||||
|         if isinstance(response, StreamingHTTPResponse): | ||||
|             await stream_callback(response) | ||||
|         else: | ||||
|             write_callback(response) | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Testing | ||||
| @@ -429,7 +504,7 @@ class Sanic: | ||||
|  | ||||
|     @property | ||||
|     def test_client(self): | ||||
|         return TestClient(self) | ||||
|         return SanicTestClient(self) | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
|     # Execution | ||||
| @@ -437,7 +512,7 @@ class Sanic: | ||||
|  | ||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, | ||||
|             after_start=None, before_stop=None, after_stop=None, ssl=None, | ||||
|             sock=None, workers=1, loop=None, protocol=HttpProtocol, | ||||
|             sock=None, workers=1, loop=None, protocol=None, | ||||
|             backlog=100, stop_event=None, register_sys_signals=True): | ||||
|         """Run the HTTP Server and listen until keyboard interrupt or term | ||||
|         signal. On termination, drain connections before closing. | ||||
| @@ -453,7 +528,8 @@ class Sanic: | ||||
|                             received before it is respected | ||||
|         :param after_stop: Functions to be executed when all requests are | ||||
|                             complete | ||||
|         :param ssl: SSLContext for SSL encryption of worker(s) | ||||
|         :param ssl: SSLContext, or location of certificate and key | ||||
|                             for SSL encryption of worker(s) | ||||
|         :param sock: Socket for the server to accept connections from | ||||
|         :param workers: Number of processes | ||||
|                             received before it is respected | ||||
| @@ -464,19 +540,27 @@ class Sanic: | ||||
|         :param protocol: Subclass of asyncio protocol class | ||||
|         :return: Nothing | ||||
|         """ | ||||
|         if protocol is None: | ||||
|             protocol = (WebSocketProtocol if self.websocket_enabled | ||||
|                         else HttpProtocol) | ||||
|         if stop_event is not None: | ||||
|             if debug: | ||||
|                 warnings.simplefilter('default') | ||||
|             warnings.warn("stop_event will be removed from future versions.", | ||||
|                           DeprecationWarning) | ||||
|         server_settings = self._helper( | ||||
|             host=host, port=port, debug=debug, before_start=before_start, | ||||
|             after_start=after_start, before_stop=before_stop, | ||||
|             after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, | ||||
|             loop=loop, protocol=protocol, backlog=backlog, | ||||
|             stop_event=stop_event, register_sys_signals=register_sys_signals) | ||||
|             register_sys_signals=register_sys_signals) | ||||
|  | ||||
|         try: | ||||
|             self.is_running = True | ||||
|             if workers == 1: | ||||
|                 serve(**server_settings) | ||||
|             else: | ||||
|                 serve_multiple(server_settings, workers, stop_event) | ||||
|                 serve_multiple(server_settings, workers) | ||||
|         except: | ||||
|             log.exception( | ||||
|                 'Experienced exception while trying to serve') | ||||
| @@ -488,26 +572,59 @@ class Sanic: | ||||
|         """This kills the Sanic""" | ||||
|         get_event_loop().stop() | ||||
|  | ||||
|     def __call__(self): | ||||
|         """gunicorn compatibility""" | ||||
|         return self | ||||
|  | ||||
|     async def create_server(self, host="127.0.0.1", port=8000, debug=False, | ||||
|                             before_start=None, after_start=None, | ||||
|                             before_stop=None, after_stop=None, ssl=None, | ||||
|                             sock=None, loop=None, protocol=HttpProtocol, | ||||
|                             sock=None, loop=None, protocol=None, | ||||
|                             backlog=100, stop_event=None): | ||||
|         """Asynchronous version of `run`. | ||||
|  | ||||
|         NOTE: This does not support multiprocessing and is not the preferred | ||||
|               way to run a Sanic application. | ||||
|         """ | ||||
|         if protocol is None: | ||||
|             protocol = (WebSocketProtocol if self.websocket_enabled | ||||
|                         else HttpProtocol) | ||||
|         if stop_event is not None: | ||||
|             if debug: | ||||
|                 warnings.simplefilter('default') | ||||
|             warnings.warn("stop_event will be removed from future versions.", | ||||
|                           DeprecationWarning) | ||||
|         server_settings = self._helper( | ||||
|             host=host, port=port, debug=debug, before_start=before_start, | ||||
|             after_start=after_start, before_stop=before_stop, | ||||
|             after_stop=after_stop, ssl=ssl, sock=sock, | ||||
|             loop=loop or get_event_loop(), protocol=protocol, | ||||
|             backlog=backlog, stop_event=stop_event, | ||||
|             run_async=True) | ||||
|             backlog=backlog, run_async=True) | ||||
|  | ||||
|         return await serve(**server_settings) | ||||
|  | ||||
|     async def _run_request_middleware(self, request): | ||||
|         # The if improves speed.  I don't know why | ||||
|         if self.request_middleware: | ||||
|             for middleware in self.request_middleware: | ||||
|                 response = middleware(request) | ||||
|                 if isawaitable(response): | ||||
|                     response = await response | ||||
|                 if response: | ||||
|                     return response | ||||
|         return None | ||||
|  | ||||
|     async def _run_response_middleware(self, request, response): | ||||
|         if self.response_middleware: | ||||
|             for middleware in self.response_middleware: | ||||
|                 _response = middleware(request, response) | ||||
|                 if isawaitable(_response): | ||||
|                     _response = await _response | ||||
|                 if _response: | ||||
|                     response = _response | ||||
|                     break | ||||
|         return response | ||||
|  | ||||
|     def _helper(self, host="127.0.0.1", port=8000, debug=False, | ||||
|                 before_start=None, after_start=None, before_stop=None, | ||||
|                 after_stop=None, ssl=None, sock=None, workers=1, loop=None, | ||||
| @@ -515,6 +632,20 @@ class Sanic: | ||||
|                 register_sys_signals=True, run_async=False): | ||||
|         """Helper function used by `run` and `create_server`.""" | ||||
|  | ||||
|         if isinstance(ssl, dict): | ||||
|             # try common aliaseses | ||||
|             cert = ssl.get('cert') or ssl.get('certificate') | ||||
|             key = ssl.get('key') or ssl.get('keyfile') | ||||
|             if cert is None or key is None: | ||||
|                 raise ValueError("SSLContext or certificate and key required.") | ||||
|             context = create_default_context(purpose=Purpose.CLIENT_AUTH) | ||||
|             context.load_cert_chain(cert, keyfile=key) | ||||
|             ssl = context | ||||
|         if stop_event is not None: | ||||
|             if debug: | ||||
|                 warnings.simplefilter('default') | ||||
|             warnings.warn("stop_event will be removed from future versions.", | ||||
|                           DeprecationWarning) | ||||
|         if loop is not None: | ||||
|             if debug: | ||||
|                 warnings.simplefilter('default') | ||||
| @@ -538,15 +669,18 @@ class Sanic: | ||||
|  | ||||
|         server_settings = { | ||||
|             'protocol': protocol, | ||||
|             'request_class': self.request_class, | ||||
|             'host': host, | ||||
|             '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 | ||||
| @@ -583,6 +717,7 @@ class Sanic: | ||||
|             server_settings['run_async'] = True | ||||
|  | ||||
|         # Serve | ||||
|         if host and port: | ||||
|             proto = "http" | ||||
|             if ssl is not None: | ||||
|                 proto = "https" | ||||
|   | ||||
| @@ -3,7 +3,9 @@ from collections import defaultdict, namedtuple | ||||
| from sanic.constants import HTTP_METHODS | ||||
| from sanic.views import CompositionView | ||||
|  | ||||
| FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host']) | ||||
| FutureRoute = namedtuple('Route', | ||||
|                          ['handler', 'uri', 'methods', | ||||
|                           'host', 'strict_slashes']) | ||||
| FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) | ||||
| FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) | ||||
| FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) | ||||
| @@ -23,6 +25,7 @@ class Blueprint: | ||||
|         self.host = host | ||||
|  | ||||
|         self.routes = [] | ||||
|         self.websocket_routes = [] | ||||
|         self.exceptions = [] | ||||
|         self.listeners = defaultdict(list) | ||||
|         self.middlewares = [] | ||||
| @@ -43,7 +46,20 @@ class Blueprint: | ||||
|             app.route( | ||||
|                 uri=uri[1:] if uri.startswith('//') else uri, | ||||
|                 methods=future.methods, | ||||
|                 host=future.host or self.host | ||||
|                 host=future.host or self.host, | ||||
|                 strict_slashes=future.strict_slashes | ||||
|                 )(future.handler) | ||||
|  | ||||
|         for future in self.websocket_routes: | ||||
|             # attach the blueprint name to the handler so that it can be | ||||
|             # prefixed properly in the router | ||||
|             future.handler.__blueprintname__ = self.name | ||||
|             # Prepend the blueprint URI prefix if available | ||||
|             uri = url_prefix + future.uri if url_prefix else future.uri | ||||
|             app.websocket( | ||||
|                 uri=uri, | ||||
|                 host=future.host or self.host, | ||||
|                 strict_slashes=future.strict_slashes | ||||
|                 )(future.handler) | ||||
|  | ||||
|         # Middleware | ||||
| @@ -70,19 +86,21 @@ class Blueprint: | ||||
|             for listener in listeners: | ||||
|                 app.listener(event)(listener) | ||||
|  | ||||
|     def route(self, uri, methods=frozenset({'GET'}), host=None): | ||||
|     def route(self, uri, methods=frozenset({'GET'}), host=None, | ||||
|               strict_slashes=False): | ||||
|         """Create a blueprint route from a decorated function. | ||||
|  | ||||
|         :param uri: endpoint at which the route will be accessible. | ||||
|         :param methods: list of acceptable HTTP methods. | ||||
|         """ | ||||
|         def decorator(handler): | ||||
|             route = FutureRoute(handler, uri, methods, host) | ||||
|             route = FutureRoute(handler, uri, methods, host, strict_slashes) | ||||
|             self.routes.append(route) | ||||
|             return handler | ||||
|         return decorator | ||||
|  | ||||
|     def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None): | ||||
|     def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, | ||||
|                   strict_slashes=False): | ||||
|         """Create a blueprint route from a function. | ||||
|  | ||||
|         :param handler: function for handling uri requests. Accepts function, | ||||
| @@ -103,7 +121,30 @@ class Blueprint: | ||||
|         if isinstance(handler, CompositionView): | ||||
|             methods = handler.handlers.keys() | ||||
|  | ||||
|         self.route(uri=uri, methods=methods, host=host)(handler) | ||||
|         self.route(uri=uri, methods=methods, host=host, | ||||
|                    strict_slashes=strict_slashes)(handler) | ||||
|         return handler | ||||
|  | ||||
|     def websocket(self, uri, host=None, strict_slashes=False): | ||||
|         """Create a blueprint websocket route from a decorated function. | ||||
|  | ||||
|         :param uri: endpoint at which the route will be accessible. | ||||
|         """ | ||||
|         def decorator(handler): | ||||
|             route = FutureRoute(handler, uri, [], host, strict_slashes) | ||||
|             self.websocket_routes.append(route) | ||||
|             return handler | ||||
|         return decorator | ||||
|  | ||||
|     def add_websocket_route(self, handler, uri, host=None): | ||||
|         """Create a blueprint websocket route from a function. | ||||
|  | ||||
|         :param handler: function for handling uri requests. Accepts function, | ||||
|                         or class instance with a view_class method. | ||||
|         :param uri: endpoint at which the route will be accessible. | ||||
|         :return: function or class instance | ||||
|         """ | ||||
|         self.websocket(uri=uri, host=host)(handler) | ||||
|         return handler | ||||
|  | ||||
|     def listener(self, event): | ||||
| @@ -149,23 +190,30 @@ class Blueprint: | ||||
|         self.statics.append(static) | ||||
|  | ||||
|     # Shorthand method decorators | ||||
|     def get(self, uri, host=None): | ||||
|         return self.route(uri, methods=["GET"], host=host) | ||||
|     def get(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=["GET"], host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def post(self, uri, host=None): | ||||
|         return self.route(uri, methods=["POST"], host=host) | ||||
|     def post(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=["POST"], host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def put(self, uri, host=None): | ||||
|         return self.route(uri, methods=["PUT"], host=host) | ||||
|     def put(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=["PUT"], host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def head(self, uri, host=None): | ||||
|         return self.route(uri, methods=["HEAD"], host=host) | ||||
|     def head(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=["HEAD"], host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def options(self, uri, host=None): | ||||
|         return self.route(uri, methods=["OPTIONS"], host=host) | ||||
|     def options(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=["OPTIONS"], host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def patch(self, uri, host=None): | ||||
|         return self.route(uri, methods=["PATCH"], host=host) | ||||
|     def patch(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=["PATCH"], host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|  | ||||
|     def delete(self, uri, host=None): | ||||
|         return self.route(uri, methods=["DELETE"], host=host) | ||||
|     def delete(self, uri, host=None, strict_slashes=False): | ||||
|         return self.route(uri, methods=["DELETE"], host=host, | ||||
|                           strict_slashes=strict_slashes) | ||||
|   | ||||
| @@ -1,9 +1,12 @@ | ||||
| import os | ||||
|  | ||||
| import types | ||||
|  | ||||
| SANIC_PREFIX = 'SANIC_' | ||||
|  | ||||
|  | ||||
| class Config(dict): | ||||
|     def __init__(self, defaults=None): | ||||
|     def __init__(self, defaults=None, load_env=True, keep_alive=True): | ||||
|         super().__init__(defaults or {}) | ||||
|         self.LOGO = """ | ||||
|                  ▄▄▄▄▄ | ||||
| @@ -28,6 +31,10 @@ 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() | ||||
|  | ||||
|     def __getattr__(self, attr): | ||||
|         try: | ||||
| @@ -90,3 +97,13 @@ class Config(dict): | ||||
|         for key in dir(obj): | ||||
|             if key.isupper(): | ||||
|                 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(): | ||||
|             if k.startswith(SANIC_PREFIX): | ||||
|                 _, config_key = k.split(SANIC_PREFIX, 1) | ||||
|                 self[config_key] = v | ||||
|   | ||||
| @@ -19,7 +19,7 @@ _Translator.update({ | ||||
|  | ||||
|  | ||||
| def _quote(str): | ||||
|     r"""Quote a string for use in a cookie header. | ||||
|     """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. | ||||
|   | ||||
| @@ -70,7 +70,7 @@ TRACEBACK_WRAPPER_HTML = ''' | ||||
|                 {frame_html} | ||||
|                 <p class="summary"> | ||||
|                 <b>{exc_name}: {exc_value}</b> | ||||
|                     while handling uri <code>{uri}</code> | ||||
|                     while handling path <code>{path}</code> | ||||
|                 </p> | ||||
|             </div> | ||||
|         </body> | ||||
|   | ||||
| @@ -16,9 +16,12 @@ from sanic.response import text, html | ||||
|  | ||||
| class ErrorHandler: | ||||
|     handlers = None | ||||
|     cached_handlers = None | ||||
|     _missing = object() | ||||
|  | ||||
|     def __init__(self): | ||||
|         self.handlers = {} | ||||
|         self.handlers = [] | ||||
|         self.cached_handlers = {} | ||||
|         self.debug = False | ||||
|  | ||||
|     def _render_traceback_html(self, exception, request): | ||||
| @@ -34,10 +37,21 @@ class ErrorHandler: | ||||
|             exc_name=exc_type.__name__, | ||||
|             exc_value=exc_value, | ||||
|             frame_html=''.join(frame_html), | ||||
|             uri=request.url) | ||||
|             path=request.path) | ||||
|  | ||||
|     def add(self, exception, handler): | ||||
|         self.handlers[exception] = handler | ||||
|         self.handlers.append((exception, handler)) | ||||
|  | ||||
|     def lookup(self, exception): | ||||
|         handler = self.cached_handlers.get(exception, self._missing) | ||||
|         if handler is self._missing: | ||||
|             for exception_class, handler in self.handlers: | ||||
|                 if isinstance(exception, exception_class): | ||||
|                     self.cached_handlers[type(exception)] = handler | ||||
|                     return handler | ||||
|             self.cached_handlers[type(exception)] = None | ||||
|             handler = None | ||||
|         return handler | ||||
|  | ||||
|     def response(self, request, exception): | ||||
|         """Fetches and executes an exception handler and returns a response | ||||
| @@ -47,9 +61,13 @@ class ErrorHandler: | ||||
|         :param exception: Exception to handle | ||||
|         :return: Response object | ||||
|         """ | ||||
|         handler = self.handlers.get(type(exception), self.default) | ||||
|         handler = self.lookup(exception) | ||||
|         response = None | ||||
|         try: | ||||
|             if handler: | ||||
|                 response = handler(request=request, exception=exception) | ||||
|             if response is None: | ||||
|                 response = self.default(request=request, exception=exception) | ||||
|         except Exception: | ||||
|             self.log(format_exc()) | ||||
|             if self.debug: | ||||
|   | ||||
| @@ -2,7 +2,7 @@ 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 urllib.parse import parse_qs, urlunparse | ||||
|  | ||||
| try: | ||||
|     from ujson import loads as json_loads | ||||
| @@ -36,24 +36,20 @@ class RequestParameters(dict): | ||||
| class Request(dict): | ||||
|     """Properties of an HTTP request such as URL, headers, etc.""" | ||||
|     __slots__ = ( | ||||
|         'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport', | ||||
|         'query_string', 'body', | ||||
|         'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', | ||||
|         '_ip', | ||||
|         'app', 'headers', 'version', 'method', '_cookies', 'transport', | ||||
|         'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', | ||||
|         '_ip', '_parsed_url', | ||||
|     ) | ||||
|  | ||||
|     def __init__(self, url_bytes, headers, version, method, transport): | ||||
|         # TODO: Content-Encoding detection | ||||
|         url_parsed = parse_url(url_bytes) | ||||
|         self._parsed_url = parse_url(url_bytes) | ||||
|         self.app = None | ||||
|         self.url = url_parsed.path.decode('utf-8') | ||||
|  | ||||
|         self.headers = headers | ||||
|         self.version = version | ||||
|         self.method = method | ||||
|         self.transport = transport | ||||
|         self.query_string = None | ||||
|         if url_parsed.query: | ||||
|             self.query_string = url_parsed.query.decode('utf-8') | ||||
|  | ||||
|         # Init but do not inhale | ||||
|         self.body = [] | ||||
| @@ -69,6 +65,8 @@ class Request(dict): | ||||
|             try: | ||||
|                 self.parsed_json = json_loads(self.body) | ||||
|             except Exception: | ||||
|                 if not self.body: | ||||
|                     return None | ||||
|                 raise InvalidUsage("Failed when parsing body as json") | ||||
|  | ||||
|         return self.parsed_json | ||||
| @@ -80,8 +78,9 @@ 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] | ||||
|         if 'Token ' in auth_header: | ||||
|             return auth_header.partition('Token ')[-1] | ||||
|         else: | ||||
|             return auth_header | ||||
|  | ||||
|     @property | ||||
| @@ -123,6 +122,10 @@ class Request(dict): | ||||
|                 self.parsed_args = RequestParameters() | ||||
|         return self.parsed_args | ||||
|  | ||||
|     @property | ||||
|     def raw_args(self): | ||||
|         return {k: v[0] for k, v in self.args.items()} | ||||
|  | ||||
|     @property | ||||
|     def cookies(self): | ||||
|         if self._cookies is None: | ||||
| @@ -142,6 +145,46 @@ class Request(dict): | ||||
|             self._ip = self.transport.get_extra_info('peername') | ||||
|         return self._ip | ||||
|  | ||||
|     @property | ||||
|     def scheme(self): | ||||
|         if self.app.websocket_enabled \ | ||||
|                 and self.headers.get('upgrade') == 'websocket': | ||||
|             scheme = 'ws' | ||||
|         else: | ||||
|             scheme = 'http' | ||||
|  | ||||
|         if self.transport.get_extra_info('sslcontext'): | ||||
|             scheme += 's' | ||||
|  | ||||
|         return scheme | ||||
|  | ||||
|     @property | ||||
|     def host(self): | ||||
|         # it appears that httptools doesn't return the host | ||||
|         # so pull it from the headers | ||||
|         return self.headers.get('Host', '') | ||||
|  | ||||
|     @property | ||||
|     def path(self): | ||||
|         return self._parsed_url.path.decode('utf-8') | ||||
|  | ||||
|     @property | ||||
|     def query_string(self): | ||||
|         if self._parsed_url.query: | ||||
|             return self._parsed_url.query.decode('utf-8') | ||||
|         else: | ||||
|             return '' | ||||
|  | ||||
|     @property | ||||
|     def url(self): | ||||
|         return urlunparse(( | ||||
|             self.scheme, | ||||
|             self.host, | ||||
|             self.path, | ||||
|             None, | ||||
|             self.query_string, | ||||
|             None)) | ||||
|  | ||||
|  | ||||
| File = namedtuple('File', ['type', 'body', 'name']) | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| from mimetypes import guess_type | ||||
| from os import path | ||||
|  | ||||
| try: | ||||
|     from ujson import dumps as json_dumps | ||||
| except: | ||||
|     from json import dumps as json_dumps | ||||
|  | ||||
| from aiofiles import open as open_async | ||||
|  | ||||
| @@ -73,37 +77,16 @@ ALL_STATUS_CODES = { | ||||
| } | ||||
|  | ||||
|  | ||||
| class HTTPResponse: | ||||
|     __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') | ||||
|  | ||||
|     def __init__(self, body=None, status=200, headers=None, | ||||
|                  content_type='text/plain', body_bytes=b''): | ||||
|         self.content_type = content_type | ||||
|  | ||||
|         if body is not None: | ||||
| class BaseHTTPResponse: | ||||
|     def _encode_body(self, data): | ||||
|         try: | ||||
|             # Try to encode it regularly | ||||
|                 self.body = body.encode() | ||||
|             return data.encode() | ||||
|         except AttributeError: | ||||
|             # Convert it to a str if you can't | ||||
|                 self.body = str(body).encode() | ||||
|         else: | ||||
|             self.body = body_bytes | ||||
|             return str(data).encode() | ||||
|  | ||||
|         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 | ||||
|         # We tried to make this as fast as possible in pure python | ||||
|         timeout_header = b'' | ||||
|         if keep_alive and keep_alive_timeout is not None: | ||||
|             timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout | ||||
|         self.headers['Content-Length'] = self.headers.get( | ||||
|             'Content-Length', len(self.body)) | ||||
|         self.headers['Content-Type'] = self.headers.get( | ||||
|             'Content-Type', self.content_type) | ||||
|     def _parse_headers(self): | ||||
|         headers = b'' | ||||
|         for name, value in self.headers.items(): | ||||
|             try: | ||||
| @@ -115,12 +98,120 @@ class HTTPResponse: | ||||
|                     b'%b: %b\r\n' % ( | ||||
|                         str(name).encode(), str(value).encode('utf-8'))) | ||||
|  | ||||
|         return headers | ||||
|  | ||||
|     @property | ||||
|     def cookies(self): | ||||
|         if self._cookies is None: | ||||
|             self._cookies = CookieJar(self.headers) | ||||
|         return self._cookies | ||||
|  | ||||
|  | ||||
| class StreamingHTTPResponse(BaseHTTPResponse): | ||||
|     __slots__ = ( | ||||
|         'transport', 'streaming_fn', | ||||
|         'status', 'content_type', 'headers', '_cookies') | ||||
|  | ||||
|     def __init__(self, streaming_fn, status=200, headers=None, | ||||
|                  content_type='text/plain'): | ||||
|         self.content_type = content_type | ||||
|         self.streaming_fn = streaming_fn | ||||
|         self.status = status | ||||
|         self.headers = headers or {} | ||||
|         self._cookies = None | ||||
|  | ||||
|     def write(self, data): | ||||
|         """Writes a chunk of data to the streaming response. | ||||
|  | ||||
|         :param data: bytes-ish data to be written. | ||||
|         """ | ||||
|         if type(data) != bytes: | ||||
|             data = self._encode_body(data) | ||||
|  | ||||
|         self.transport.write( | ||||
|             b"%x\r\n%b\r\n" % (len(data), data)) | ||||
|  | ||||
|     async def stream( | ||||
|             self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||
|         """Streams headers, runs the `streaming_fn` callback that writes | ||||
|         content to the response body, then finalizes the response body. | ||||
|         """ | ||||
|         headers = self.get_headers( | ||||
|             version, keep_alive=keep_alive, | ||||
|             keep_alive_timeout=keep_alive_timeout) | ||||
|         self.transport.write(headers) | ||||
|  | ||||
|         await self.streaming_fn(self) | ||||
|         self.transport.write(b'0\r\n\r\n') | ||||
|  | ||||
|     def get_headers( | ||||
|             self, version="1.1", keep_alive=False, keep_alive_timeout=None): | ||||
|         # This is all returned in a kind-of funky way | ||||
|         # We tried to make this as fast as possible in pure python | ||||
|         timeout_header = b'' | ||||
|         if keep_alive and keep_alive_timeout is not None: | ||||
|             timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout | ||||
|  | ||||
|         self.headers['Transfer-Encoding'] = 'chunked' | ||||
|         self.headers.pop('Content-Length', None) | ||||
|         self.headers['Content-Type'] = self.headers.get( | ||||
|             'Content-Type', self.content_type) | ||||
|  | ||||
|         headers = self._parse_headers() | ||||
|  | ||||
|         # 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'%b' | ||||
|                 b'%b\r\n') % ( | ||||
|                    version.encode(), | ||||
|                    self.status, | ||||
|                    status, | ||||
|                    timeout_header, | ||||
|                    headers | ||||
|                ) | ||||
|  | ||||
|  | ||||
| class HTTPResponse(BaseHTTPResponse): | ||||
|     __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') | ||||
|  | ||||
|     def __init__(self, body=None, status=200, headers=None, | ||||
|                  content_type='text/plain', body_bytes=b''): | ||||
|         self.content_type = content_type | ||||
|  | ||||
|         if body is not None: | ||||
|             self.body = self._encode_body(body) | ||||
|         else: | ||||
|             self.body = body_bytes | ||||
|  | ||||
|         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 | ||||
|         # We tried to make this as fast as possible in pure python | ||||
|         timeout_header = b'' | ||||
|         if keep_alive and keep_alive_timeout is not None: | ||||
|             timeout_header = b'Keep-Alive: %d\r\n' % keep_alive_timeout | ||||
|         self.headers['Content-Length'] = self.headers.get( | ||||
|             'Content-Length', len(self.body)) | ||||
|         self.headers['Content-Type'] = self.headers.get( | ||||
|             'Content-Type', self.content_type) | ||||
|  | ||||
|         headers = self._parse_headers() | ||||
|  | ||||
|         # 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, b'UNKNOWN RESPONSE') | ||||
|  | ||||
|         return (b'HTTP/%b %d %b\r\n' | ||||
|                 b'Connection: %b\r\n' | ||||
|                 b'%b' | ||||
| @@ -161,10 +252,10 @@ def text(body, status=200, headers=None, | ||||
|     :param body: Response data to be encoded. | ||||
|     :param status: Response code. | ||||
|     :param headers: Custom Headers. | ||||
|     :param content_type: | ||||
|         the content type (string) of the response | ||||
|     :param content_type: the content type (string) of the response | ||||
|     """ | ||||
|     return HTTPResponse(body, status=status, headers=headers, | ||||
|     return HTTPResponse( | ||||
|         body, status=status, headers=headers, | ||||
|         content_type=content_type) | ||||
|  | ||||
|  | ||||
| @@ -175,8 +266,7 @@ def raw(body, status=200, headers=None, | ||||
|     :param body: Response data. | ||||
|     :param status: Response code. | ||||
|     :param headers: Custom Headers. | ||||
|     :param content_type: | ||||
|         the content type (string) of the response | ||||
|     :param content_type: the content type (string) of the response. | ||||
|     """ | ||||
|     return HTTPResponse(body_bytes=body, status=status, headers=headers, | ||||
|                         content_type=content_type) | ||||
| @@ -220,6 +310,35 @@ async def file(location, mime_type=None, headers=None, _range=None): | ||||
|                         body_bytes=out_stream) | ||||
|  | ||||
|  | ||||
| def stream( | ||||
|         streaming_fn, status=200, headers=None, | ||||
|         content_type="text/plain; charset=utf-8"): | ||||
|     """Accepts an coroutine `streaming_fn` which can be used to | ||||
|     write chunks to a streaming response. Returns a `StreamingHTTPResponse`. | ||||
|  | ||||
|     Example usage:: | ||||
|  | ||||
|         @app.route("/") | ||||
|         async def index(request): | ||||
|             async def streaming_fn(response): | ||||
|                 await response.write('foo') | ||||
|                 await response.write('bar') | ||||
|  | ||||
|             return stream(streaming_fn, content_type='text/plain') | ||||
|  | ||||
|     :param streaming_fn: A coroutine accepts a response and | ||||
|         writes content to that response. | ||||
|     :param mime_type: Specific mime_type. | ||||
|     :param headers: Custom Headers. | ||||
|     """ | ||||
|     return StreamingHTTPResponse( | ||||
|         streaming_fn, | ||||
|         headers=headers, | ||||
|         content_type=content_type, | ||||
|         status=status | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def redirect(to, headers=None, status=302, | ||||
|              content_type="text/html; charset=utf-8"): | ||||
|     """Abort execution and cause a 302 redirect (by default). | ||||
|   | ||||
| @@ -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,12 +72,14 @@ 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 | ||||
|  | ||||
|         For example: | ||||
|         `parse_parameter_string('<param_one:[A-z]>')` -> | ||||
|         For example:: | ||||
|  | ||||
|             parse_parameter_string('<param_one:[A-z]>')` -> | ||||
|                 ('param_one', str, '[A-z]') | ||||
|  | ||||
|         :param parameter_string: String to parse | ||||
| @@ -95,9 +98,15 @@ class Router: | ||||
|  | ||||
|         return name, _type, pattern | ||||
|  | ||||
|     def add(self, uri, methods, handler, host=None): | ||||
|     def add(self, uri, methods, handler, host=None, strict_slashes=False): | ||||
|  | ||||
|         # add regular version | ||||
|         self._add(uri, methods, handler, host) | ||||
|  | ||||
|         if strict_slashes: | ||||
|             return | ||||
|  | ||||
|         # Add versions with and without trailing / | ||||
|         slash_is_missing = ( | ||||
|             not uri[-1] == '/' | ||||
|             and not self.routes_all.get(uri + '/', False) | ||||
| @@ -137,9 +146,6 @@ class Router: | ||||
|                 for host_ in host: | ||||
|                     self.add(uri, methods, handler, host_) | ||||
|                 return | ||||
|         else: | ||||
|             # default host | ||||
|             self.hosts.add('*') | ||||
|  | ||||
|         # Dict for faster lookups of if method allowed | ||||
|         if methods: | ||||
| @@ -157,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) | ||||
| @@ -281,14 +287,14 @@ class Router: | ||||
|         """ | ||||
|         # No virtual hosts specified; default behavior | ||||
|         if not self.hosts: | ||||
|             return self._get(request.url, request.method, '') | ||||
|             return self._get(request.path, request.method, '') | ||||
|         # virtual hosts specified; try to match route to the host header | ||||
|         try: | ||||
|             return self._get(request.url, request.method, | ||||
|             return self._get(request.path, request.method, | ||||
|                              request.headers.get("Host", '')) | ||||
|         # try default hosts | ||||
|         except NotFound: | ||||
|             return self._get(request.url, request.method, '') | ||||
|             return self._get(request.path, request.method, '') | ||||
|  | ||||
|     @lru_cache(maxsize=ROUTER_CACHE_SIZE) | ||||
|     def _get(self, url, method, host): | ||||
|   | ||||
							
								
								
									
										108
									
								
								sanic/server.py
									
									
									
									
									
								
							
							
						
						
									
										108
									
								
								sanic/server.py
									
									
									
									
									
								
							| @@ -4,11 +4,17 @@ import traceback | ||||
| import warnings | ||||
| from functools import partial | ||||
| from inspect import isawaitable | ||||
| from multiprocessing import Process, Event | ||||
| from os import set_inheritable | ||||
| from signal import SIGTERM, SIGINT | ||||
| from signal import signal as signal_func | ||||
| from socket import socket, SOL_SOCKET, SO_REUSEADDR | ||||
| from multiprocessing import Process | ||||
| from signal import ( | ||||
|     SIGTERM, SIGINT, | ||||
|     signal as signal_func, | ||||
|     Signals | ||||
| ) | ||||
| from socket import ( | ||||
|     socket, | ||||
|     SOL_SOCKET, | ||||
|     SO_REUSEADDR, | ||||
| ) | ||||
| from time import time | ||||
|  | ||||
| from httptools import HttpRequestParser | ||||
| @@ -58,12 +64,14 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         'parser', 'request', 'url', 'headers', | ||||
|         # request config | ||||
|         'request_handler', 'request_timeout', 'request_max_size', | ||||
|         'request_class', | ||||
|         # connection management | ||||
|         '_total_request_size', '_timeout_handler', '_last_communication_time') | ||||
|  | ||||
|     def __init__(self, *, loop, request_handler, error_handler, | ||||
|                  signal=Signal(), connections=set(), request_timeout=60, | ||||
|                  request_max_size=None): | ||||
|                  request_max_size=None, request_class=None, | ||||
|                  keep_alive=True): | ||||
|         self.loop = loop | ||||
|         self.transport = None | ||||
|         self.request = None | ||||
| @@ -76,10 +84,18 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         self.error_handler = error_handler | ||||
|         self.request_timeout = request_timeout | ||||
|         self.request_max_size = request_max_size | ||||
|         self.request_class = request_class or Request | ||||
|         self._total_request_size = 0 | ||||
|         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._keep_alive | ||||
|                 and not self.signal.stopped | ||||
|                 and self.parser.should_keep_alive()) | ||||
|  | ||||
|     # -------------------------------------------- # | ||||
|     # Connection | ||||
| @@ -145,7 +161,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|         self.headers.append((name.decode().casefold(), value.decode())) | ||||
|  | ||||
|     def on_headers_complete(self): | ||||
|         self.request = Request( | ||||
|         self.request = self.request_class( | ||||
|             url_bytes=self.url, | ||||
|             headers=CIDict(self.headers), | ||||
|             version=self.parser.get_http_version(), | ||||
| @@ -159,20 +175,59 @@ class HttpProtocol(asyncio.Protocol): | ||||
|     def on_message_complete(self): | ||||
|         if self.request.body: | ||||
|             self.request.body = b''.join(self.request.body) | ||||
|  | ||||
|         self._request_handler_task = self.loop.create_task( | ||||
|             self.request_handler(self.request, self.write_response)) | ||||
|             self.request_handler( | ||||
|                 self.request, | ||||
|                 self.write_response, | ||||
|                 self.stream_response)) | ||||
|  | ||||
|     # -------------------------------------------- # | ||||
|     # Responding | ||||
|     # -------------------------------------------- # | ||||
|  | ||||
|     def write_response(self, response): | ||||
|         keep_alive = ( | ||||
|             self.parser.should_keep_alive() and not self.signal.stopped) | ||||
|         """ | ||||
|         Writes response content synchronously to the transport. | ||||
|         """ | ||||
|         try: | ||||
|             keep_alive = self.keep_alive | ||||
|             self.transport.write( | ||||
|                 response.output( | ||||
|                     self.request.version, keep_alive, self.request_timeout)) | ||||
|                     self.request.version, keep_alive, | ||||
|                     self.request_timeout)) | ||||
|         except AttributeError: | ||||
|             log.error( | ||||
|                 ('Invalid response object for url {}, ' | ||||
|                  'Expected Type: HTTPResponse, Actual Type: {}').format( | ||||
|                     self.url, type(response))) | ||||
|             self.write_error(ServerError('Invalid response type')) | ||||
|         except RuntimeError: | ||||
|             log.error( | ||||
|                 'Connection lost before response written @ {}'.format( | ||||
|                     self.request.ip)) | ||||
|         except Exception as e: | ||||
|             self.bail_out( | ||||
|                 "Writing response failed, connection closed {}".format( | ||||
|                     repr(e))) | ||||
|         finally: | ||||
|             if not keep_alive: | ||||
|                 self.transport.close() | ||||
|             else: | ||||
|                 self._last_request_time = current_time | ||||
|                 self.cleanup() | ||||
|  | ||||
|     async def stream_response(self, response): | ||||
|         """ | ||||
|         Streams a response to the client asynchronously. Attaches | ||||
|         the transport to the response so the response consumer can | ||||
|         write to the response as needed. | ||||
|         """ | ||||
|  | ||||
|         try: | ||||
|             keep_alive = self.keep_alive | ||||
|             response.transport = self.transport | ||||
|             await response.stream( | ||||
|                 self.request.version, keep_alive, self.request_timeout) | ||||
|         except AttributeError: | ||||
|             log.error( | ||||
|                 ('Invalid response object for url {}, ' | ||||
| @@ -191,7 +246,6 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             if not keep_alive: | ||||
|                 self.transport.close() | ||||
|             else: | ||||
|                 # Record that we received data | ||||
|                 self._last_request_time = current_time | ||||
|                 self.cleanup() | ||||
|  | ||||
| @@ -212,7 +266,7 @@ class HttpProtocol(asyncio.Protocol): | ||||
|             self.transport.close() | ||||
|  | ||||
|     def bail_out(self, message, from_error=False): | ||||
|         if from_error and self.transport.is_closing(): | ||||
|         if from_error or self.transport.is_closing(): | ||||
|             log.error( | ||||
|                 ("Transport closed @ {} and exception " | ||||
|                  "experienced during error handling").format( | ||||
| @@ -271,7 +325,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|           after_start=None, before_stop=None, after_stop=None, debug=False, | ||||
|           request_timeout=60, ssl=None, sock=None, request_max_size=None, | ||||
|           reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, | ||||
|           register_sys_signals=True, run_async=False): | ||||
|           register_sys_signals=True, run_async=False, connections=None, | ||||
|           signal=Signal(), request_class=None, keep_alive=True): | ||||
|     """Start asynchronous HTTP Server on an individual process. | ||||
|  | ||||
|     :param host: Address to host on | ||||
| @@ -296,6 +351,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|     :param reuse_port: `True` for multiple workers | ||||
|     :param loop: asyncio compatible event loop | ||||
|     :param protocol: subclass of asyncio protocol class | ||||
|     :param request_class: Request class to use | ||||
|     :return: Nothing | ||||
|     """ | ||||
|     if not run_async: | ||||
| @@ -307,8 +363,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|  | ||||
|     trigger_events(before_start, loop) | ||||
|  | ||||
|     connections = set() | ||||
|     signal = Signal() | ||||
|     connections = connections if connections is not None else set() | ||||
|     server = partial( | ||||
|         protocol, | ||||
|         loop=loop, | ||||
| @@ -318,6 +373,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|         error_handler=error_handler, | ||||
|         request_timeout=request_timeout, | ||||
|         request_max_size=request_max_size, | ||||
|         request_class=request_class, | ||||
|         keep_alive=keep_alive, | ||||
|     ) | ||||
|  | ||||
|     server_coroutine = loop.create_server( | ||||
| @@ -379,7 +436,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | ||||
|         loop.close() | ||||
|  | ||||
|  | ||||
| def serve_multiple(server_settings, workers, stop_event=None): | ||||
| def serve_multiple(server_settings, workers): | ||||
|     """Start multiple server processes simultaneously.  Stop on interrupt | ||||
|     and terminate signals, and drain connections when complete. | ||||
|  | ||||
| @@ -396,19 +453,22 @@ def serve_multiple(server_settings, workers, stop_event=None): | ||||
|                       " has more information.", DeprecationWarning) | ||||
|     server_settings['reuse_port'] = True | ||||
|  | ||||
|     # Handling when custom socket is not provided. | ||||
|     if server_settings.get('sock') is None: | ||||
|         sock = socket() | ||||
|         sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) | ||||
|         sock.bind((server_settings['host'], server_settings['port'])) | ||||
|     set_inheritable(sock.fileno(), True) | ||||
|         sock.set_inheritable(True) | ||||
|         server_settings['sock'] = sock | ||||
|         server_settings['host'] = None | ||||
|         server_settings['port'] = None | ||||
|  | ||||
|     if stop_event is None: | ||||
|         stop_event = Event() | ||||
|     def sig_handler(signal, frame): | ||||
|         log.info("Received signal {}. Shutting down.".format( | ||||
|             Signals(signal).name)) | ||||
|  | ||||
|     signal_func(SIGINT, lambda s, f: stop_event.set()) | ||||
|     signal_func(SIGTERM, lambda s, f: stop_event.set()) | ||||
|     signal_func(SIGINT, lambda s, f: sig_handler(s, f)) | ||||
|     signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) | ||||
|  | ||||
|     processes = [] | ||||
|     for _ in range(workers): | ||||
| @@ -423,6 +483,6 @@ def serve_multiple(server_settings, workers, stop_event=None): | ||||
|     # the above processes will block this until they're stopped | ||||
|     for process in processes: | ||||
|         process.terminate() | ||||
|     sock.close() | ||||
|     server_settings.get('sock').close() | ||||
|  | ||||
|     asyncio.get_event_loop().stop() | ||||
|   | ||||
| @@ -48,14 +48,18 @@ def register(app, uri, file_or_directory, pattern, | ||||
|         # 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 = file_or_directory | ||||
|         root_path = file_path = file_or_directory | ||||
|         if file_uri: | ||||
|             file_path = path.join( | ||||
|                 file_or_directory, sub('^[/]*', '', file_uri)) | ||||
|  | ||||
|         # 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 = unquote(file_path) | ||||
|         file_path = path.abspath(unquote(file_path)) | ||||
|         if not file_path.startswith(path.abspath(unquote(root_path))): | ||||
|             raise FileNotFound('File not found', | ||||
|                                path=file_or_directory, | ||||
|                                relative_url=file_uri) | ||||
|         try: | ||||
|             headers = {} | ||||
|             # Check if the client has been sent this file before | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| import traceback | ||||
|  | ||||
| from sanic.log import log | ||||
|  | ||||
| HOST = '127.0.0.1' | ||||
| PORT = 42101 | ||||
|  | ||||
|  | ||||
| class TestClient: | ||||
| class SanicTestClient: | ||||
|     def __init__(self, app): | ||||
|         self.app = app | ||||
|  | ||||
| @@ -17,10 +19,15 @@ class TestClient: | ||||
|                 host=HOST, port=PORT, uri=uri) | ||||
|  | ||||
|         log.info(url) | ||||
|         async with aiohttp.ClientSession(cookies=cookies) as session: | ||||
|         conn = aiohttp.TCPConnector(verify_ssl=False) | ||||
|         async with aiohttp.ClientSession( | ||||
|                 cookies=cookies, connector=conn) as session: | ||||
|             async with getattr( | ||||
|                     session, method.lower())(url, *args, **kwargs) as response: | ||||
|                 try: | ||||
|                     response.text = await response.text() | ||||
|                 except UnicodeDecodeError as e: | ||||
|                     response.text = None | ||||
|                 response.body = await response.read() | ||||
|                 return response | ||||
|  | ||||
| @@ -45,6 +52,8 @@ class TestClient: | ||||
|                     **request_kwargs) | ||||
|                 results[-1] = response | ||||
|             except Exception as e: | ||||
|                 log.error( | ||||
|                     'Exception:\n{}'.format(traceback.format_exc())) | ||||
|                 exceptions.append(e) | ||||
|             self.app.stop() | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import warnings | ||||
|  | ||||
| from sanic.testing import TestClient | ||||
| from sanic.testing import SanicTestClient | ||||
|  | ||||
|  | ||||
| def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||
| @@ -11,7 +11,7 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | ||||
|         "the next major version after 0.4.0.  Please use the `test_client` " | ||||
|         "available on the app object.", DeprecationWarning) | ||||
|  | ||||
|     test_client = TestClient(app) | ||||
|     test_client = SanicTestClient(app) | ||||
|     return test_client._sanic_endpoint_test( | ||||
|         method, uri, gather_request, debug, server_kwargs, | ||||
|         *request_args, **request_kwargs) | ||||
|   | ||||
							
								
								
									
										67
									
								
								sanic/websocket.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								sanic/websocket.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,67 @@ | ||||
| from sanic.exceptions import InvalidUsage | ||||
| from sanic.server import HttpProtocol | ||||
| from httptools import HttpParserUpgrade | ||||
| from websockets import handshake, WebSocketCommonProtocol, InvalidHandshake | ||||
| from websockets import ConnectionClosed  # noqa | ||||
|  | ||||
|  | ||||
| class WebSocketProtocol(HttpProtocol): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.websocket = None | ||||
|  | ||||
|     def connection_timeout(self): | ||||
|         # timeouts make no sense for websocket routes | ||||
|         if self.websocket is None: | ||||
|             super().connection_timeout() | ||||
|  | ||||
|     def connection_lost(self, exc): | ||||
|         if self.websocket is not None: | ||||
|             self.websocket.connection_lost(exc) | ||||
|         super().connection_lost(exc) | ||||
|  | ||||
|     def data_received(self, data): | ||||
|         if self.websocket is not None: | ||||
|             # pass the data to the websocket protocol | ||||
|             self.websocket.data_received(data) | ||||
|         else: | ||||
|             try: | ||||
|                 super().data_received(data) | ||||
|             except HttpParserUpgrade: | ||||
|                 # this is okay, it just indicates we've got an upgrade request | ||||
|                 pass | ||||
|  | ||||
|     def write_response(self, response): | ||||
|         if self.websocket is not None: | ||||
|             # websocket requests do not write a response | ||||
|             self.transport.close() | ||||
|         else: | ||||
|             super().write_response(response) | ||||
|  | ||||
|     async def websocket_handshake(self, request): | ||||
|         # let the websockets package do the handshake with the client | ||||
|         headers = [] | ||||
|  | ||||
|         def get_header(k): | ||||
|             return request.headers.get(k, '') | ||||
|  | ||||
|         def set_header(k, v): | ||||
|             headers.append((k, v)) | ||||
|  | ||||
|         try: | ||||
|             key = handshake.check_request(get_header) | ||||
|             handshake.build_response(set_header, key) | ||||
|         except InvalidHandshake: | ||||
|             raise InvalidUsage('Invalid websocket request') | ||||
|  | ||||
|         # write the 101 response back to the client | ||||
|         rv = b'HTTP/1.1 101 Switching Protocols\r\n' | ||||
|         for k, v in headers: | ||||
|             rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n' | ||||
|         rv += b'\r\n' | ||||
|         request.transport.write(rv) | ||||
|  | ||||
|         # hook up the websocket protocol | ||||
|         self.websocket = WebSocketCommonProtocol() | ||||
|         self.websocket.connection_made(request.transport) | ||||
|         return self.websocket | ||||
							
								
								
									
										166
									
								
								sanic/worker.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								sanic/worker.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,166 @@ | ||||
| import os | ||||
| import sys | ||||
| import signal | ||||
| import asyncio | ||||
| import logging | ||||
|  | ||||
| try: | ||||
|     import ssl | ||||
| except ImportError: | ||||
|     ssl = None | ||||
|  | ||||
| import uvloop | ||||
| import gunicorn.workers.base as base | ||||
|  | ||||
| from sanic.server import trigger_events, serve, HttpProtocol, Signal | ||||
| from sanic.websocket import WebSocketProtocol | ||||
|  | ||||
|  | ||||
| class GunicornWorker(base.Worker): | ||||
|  | ||||
|     def __init__(self, *args, **kw):  # pragma: no cover | ||||
|         super().__init__(*args, **kw) | ||||
|         cfg = self.cfg | ||||
|         if cfg.is_ssl: | ||||
|             self.ssl_context = self._create_ssl_context(cfg) | ||||
|         else: | ||||
|             self.ssl_context = None | ||||
|         self.servers = [] | ||||
|         self.connections = set() | ||||
|         self.exit_code = 0 | ||||
|         self.signal = Signal() | ||||
|  | ||||
|     def init_process(self): | ||||
|         # create new event_loop after fork | ||||
|         asyncio.get_event_loop().close() | ||||
|  | ||||
|         asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) | ||||
|         self.loop = asyncio.new_event_loop() | ||||
|         asyncio.set_event_loop(self.loop) | ||||
|  | ||||
|         super().init_process() | ||||
|  | ||||
|     def run(self): | ||||
|         is_debug = self.log.loglevel == logging.DEBUG | ||||
|         protocol = (WebSocketProtocol if self.app.callable.websocket_enabled | ||||
|                     else HttpProtocol) | ||||
|         self._server_settings = self.app.callable._helper( | ||||
|             host=None, | ||||
|             port=None, | ||||
|             loop=self.loop, | ||||
|             debug=is_debug, | ||||
|             protocol=protocol, | ||||
|             ssl=self.ssl_context, | ||||
|             run_async=True) | ||||
|         self._server_settings['signal'] = self.signal | ||||
|         self._server_settings.pop('sock') | ||||
|         trigger_events(self._server_settings.get('before_start', []), | ||||
|                        self.loop) | ||||
|         self._server_settings['before_start'] = () | ||||
|  | ||||
|         self._runner = asyncio.ensure_future(self._run(), loop=self.loop) | ||||
|         try: | ||||
|             self.loop.run_until_complete(self._runner) | ||||
|             self.app.callable.is_running = True | ||||
|             trigger_events(self._server_settings.get('after_start', []), | ||||
|                            self.loop) | ||||
|             self.loop.run_until_complete(self._check_alive()) | ||||
|             trigger_events(self._server_settings.get('before_stop', []), | ||||
|                            self.loop) | ||||
|             self.loop.run_until_complete(self.close()) | ||||
|         finally: | ||||
|             trigger_events(self._server_settings.get('after_stop', []), | ||||
|                            self.loop) | ||||
|             self.loop.close() | ||||
|  | ||||
|         sys.exit(self.exit_code) | ||||
|  | ||||
|     async def close(self): | ||||
|         if self.servers: | ||||
|             # stop accepting connections | ||||
|             self.log.info("Stopping server: %s, connections: %s", | ||||
|                           self.pid, len(self.connections)) | ||||
|             for server in self.servers: | ||||
|                 server.close() | ||||
|                 await server.wait_closed() | ||||
|             self.servers.clear() | ||||
|  | ||||
|             # prepare connections for closing | ||||
|             self.signal.stopped = True | ||||
|             for conn in self.connections: | ||||
|                 conn.close_if_idle() | ||||
|  | ||||
|             while self.connections: | ||||
|                 await asyncio.sleep(0.1) | ||||
|  | ||||
|     async def _run(self): | ||||
|         for sock in self.sockets: | ||||
|             self.servers.append(await serve( | ||||
|                 sock=sock, | ||||
|                 connections=self.connections, | ||||
|                 **self._server_settings | ||||
|             )) | ||||
|  | ||||
|     async def _check_alive(self): | ||||
|         # If our parent changed then we shut down. | ||||
|         pid = os.getpid() | ||||
|         try: | ||||
|             while self.alive: | ||||
|                 self.notify() | ||||
|  | ||||
|                 if pid == os.getpid() and self.ppid != os.getppid(): | ||||
|                     self.alive = False | ||||
|                     self.log.info("Parent changed, shutting down: %s", self) | ||||
|                 else: | ||||
|                     await asyncio.sleep(1.0, loop=self.loop) | ||||
|         except (Exception, BaseException, GeneratorExit, KeyboardInterrupt): | ||||
|             pass | ||||
|  | ||||
|     @staticmethod | ||||
|     def _create_ssl_context(cfg): | ||||
|         """ Creates SSLContext instance for usage in asyncio.create_server. | ||||
|         See ssl.SSLSocket.__init__ for more details. | ||||
|         """ | ||||
|         ctx = ssl.SSLContext(cfg.ssl_version) | ||||
|         ctx.load_cert_chain(cfg.certfile, cfg.keyfile) | ||||
|         ctx.verify_mode = cfg.cert_reqs | ||||
|         if cfg.ca_certs: | ||||
|             ctx.load_verify_locations(cfg.ca_certs) | ||||
|         if cfg.ciphers: | ||||
|             ctx.set_ciphers(cfg.ciphers) | ||||
|         return ctx | ||||
|  | ||||
|     def init_signals(self): | ||||
|         # Set up signals through the event loop API. | ||||
|  | ||||
|         self.loop.add_signal_handler(signal.SIGQUIT, self.handle_quit, | ||||
|                                      signal.SIGQUIT, None) | ||||
|  | ||||
|         self.loop.add_signal_handler(signal.SIGTERM, self.handle_exit, | ||||
|                                      signal.SIGTERM, None) | ||||
|  | ||||
|         self.loop.add_signal_handler(signal.SIGINT, self.handle_quit, | ||||
|                                      signal.SIGINT, None) | ||||
|  | ||||
|         self.loop.add_signal_handler(signal.SIGWINCH, self.handle_winch, | ||||
|                                      signal.SIGWINCH, None) | ||||
|  | ||||
|         self.loop.add_signal_handler(signal.SIGUSR1, self.handle_usr1, | ||||
|                                      signal.SIGUSR1, None) | ||||
|  | ||||
|         self.loop.add_signal_handler(signal.SIGABRT, self.handle_abort, | ||||
|                                      signal.SIGABRT, None) | ||||
|  | ||||
|         # Don't let SIGTERM and SIGUSR1 disturb active requests | ||||
|         # by interrupting system calls | ||||
|         signal.siginterrupt(signal.SIGTERM, False) | ||||
|         signal.siginterrupt(signal.SIGUSR1, False) | ||||
|  | ||||
|     def handle_quit(self, sig, frame): | ||||
|         self.alive = False | ||||
|         self.cfg.worker_int(self) | ||||
|  | ||||
|     def handle_abort(self, sig, frame): | ||||
|         self.alive = False | ||||
|         self.exit_code = 1 | ||||
|         self.cfg.worker_abort(self) | ||||
							
								
								
									
										66
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										66
									
								
								setup.py
									
									
									
									
									
								
							| @@ -4,8 +4,10 @@ Sanic | ||||
| import codecs | ||||
| import os | ||||
| import re | ||||
| from setuptools import setup | ||||
| from distutils.errors import DistutilsPlatformError | ||||
| from distutils.util import strtobool | ||||
|  | ||||
| from setuptools import setup | ||||
|  | ||||
| with codecs.open(os.path.join(os.path.abspath(os.path.dirname( | ||||
|         __file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp: | ||||
| @@ -15,32 +17,50 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname( | ||||
|     except IndexError: | ||||
|         raise RuntimeError('Unable to determine version.') | ||||
|  | ||||
| install_requires = [ | ||||
|     'httptools>=0.0.9', | ||||
|     'ujson>=1.35', | ||||
|     'aiofiles>=0.3.0', | ||||
| ] | ||||
|  | ||||
| if os.name != 'nt': | ||||
|     install_requires.append('uvloop>=0.5.3') | ||||
|  | ||||
| setup( | ||||
|     name='sanic', | ||||
|     version=version, | ||||
|     url='http://github.com/channelcat/sanic/', | ||||
|     license='MIT', | ||||
|     author='Channel Cat', | ||||
|     author_email='channelcat@gmail.com', | ||||
|     description=( | ||||
| setup_kwargs = { | ||||
|     'name': 'sanic', | ||||
|     'version': version, | ||||
|     'url': 'http://github.com/channelcat/sanic/', | ||||
|     'license': 'MIT', | ||||
|     'author': 'Channel Cat', | ||||
|     'author_email': 'channelcat@gmail.com', | ||||
|     'description': ( | ||||
|         'A microframework based on uvloop, httptools, and learnings of flask'), | ||||
|     packages=['sanic'], | ||||
|     platforms='any', | ||||
|     install_requires=install_requires, | ||||
|     classifiers=[ | ||||
|     'packages': ['sanic'], | ||||
|     'platforms': 'any', | ||||
|     'classifiers': [ | ||||
|         'Development Status :: 2 - Pre-Alpha', | ||||
|         'Environment :: Web Environment', | ||||
|         'License :: OSI Approved :: MIT License', | ||||
|         'Programming Language :: Python :: 3.5', | ||||
|         'Programming Language :: Python :: 3.6', | ||||
|     ], | ||||
| ) | ||||
| } | ||||
|  | ||||
| ujson = 'ujson>=1.35' | ||||
| uvloop = 'uvloop>=0.5.3' | ||||
|  | ||||
| requirements = [ | ||||
|     'httptools>=0.0.9', | ||||
|     uvloop, | ||||
|     ujson, | ||||
|     'aiofiles>=0.3.0', | ||||
|     'websockets>=3.2', | ||||
| ] | ||||
| if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): | ||||
|     print("Installing without uJSON") | ||||
|     requirements.remove(ujson) | ||||
|  | ||||
| if strtobool(os.environ.get("SANIC_NO_UVLOOP", "no")): | ||||
|     print("Installing without uvLoop") | ||||
|     requirements.remove(uvloop) | ||||
|  | ||||
| try: | ||||
|     setup_kwargs['install_requires'] = requirements | ||||
|     setup(**setup_kwargs) | ||||
| except DistutilsPlatformError as exception: | ||||
|     requirements.remove(ujson) | ||||
|     requirements.remove(uvloop) | ||||
|     print("Installing without uJSON or uvLoop") | ||||
|     setup_kwargs['install_requires'] = requirements | ||||
|     setup(**setup_kwargs) | ||||
|   | ||||
							
								
								
									
										22
									
								
								tests/certs/selfsigned.cert
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								tests/certs/selfsigned.cert
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| -----BEGIN CERTIFICATE----- | ||||
| MIIDtTCCAp2gAwIBAgIJAO6wb0FSc/rNMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV | ||||
| BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX | ||||
| aWRnaXRzIFB0eSBMdGQwHhcNMTcwMzAzMTUyODAzWhcNMTkxMTI4MTUyODAzWjBF | ||||
| MQswCQYDVQQGEwJVUzETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 | ||||
| ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB | ||||
| CgKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7mdB6Bflh | ||||
| V5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLwdpaoTGqE | ||||
| vYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49IAouBHq3 | ||||
| h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ8gjaC8/1 | ||||
| w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJPXtNrTwf | ||||
| qEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABo4GnMIGkMB0GA1UdDgQWBBRa46Ix | ||||
| 9s9tmMqu+Zz1mocHghm4NTB1BgNVHSMEbjBsgBRa46Ix9s9tmMqu+Zz1mocHghm4 | ||||
| NaFJpEcwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNV | ||||
| BAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAO6wb0FSc/rNMAwGA1UdEwQF | ||||
| MAMBAf8wDQYJKoZIhvcNAQELBQADggEBACdrnM8zb7abxAJsU5WLn1IR0f2+EFA7 | ||||
| ezBEJBM4bn0IZrXuP5ThZ2wieJlshG0C16XN9+zifavHci+AtQwWsB0f/ppHdvWQ | ||||
| 7wt7JN88w+j0DNIYEadRCjWxR3gRAXPgKu3sdyScKFq8MvB49A2EdXRmQSTIM6Fj | ||||
| teRbE+poxewFT0mhurf3xrtGiSALmv7uAzhRDqpYUzcUlbOGgkyFLYAOOdvZvei+ | ||||
| mfXDi4HKYxgyv53JxBARMdajnCHXM7zQ6Tjc8j1HRtmDQ3XapUB559KfxfODGQq5 | ||||
| zmeoZWU4duxcNXJM0Eiz1CJ39JoWwi8sqaGi/oskuyAh7YKyVTn8xa8= | ||||
| -----END CERTIFICATE----- | ||||
							
								
								
									
										27
									
								
								tests/certs/selfsigned.key
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								tests/certs/selfsigned.key
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| -----BEGIN RSA PRIVATE KEY----- | ||||
| MIIEpAIBAAKCAQEAsy7Zb3p4yCEnUtPLwqeJrwj9u/ZmcFCrMAktFBx9hG6rY2r7 | ||||
| mdB6BflhV5cUJXxnsNiDpYcxGhA8kry7pEork1vZ05DyZC9ulVlvxBouVShBcLLw | ||||
| dpaoTGqEvYtejv6x7ogwMXOjkWWb1WpOv4CVhpeXJ7O/d1uAiYgcUpTpPp4ONG49 | ||||
| IAouBHq3h+o4nVvNfB0J8gaCtTsTZqi1Wt8WYs3XjxGJaKh//ealfRe1kuv40CWQ | ||||
| 8gjaC8/1w9pHdom3Wi/RwfDM3+dVGV6M5lAbPXMB4RK17Hk9P3hlJxJOpKBdgcBJ | ||||
| PXtNrTwfqEWWxk2mB/YVyB84AxjkkNoYyi2ggQIDAQABAoIBAFgVasxTf3aaXbNo | ||||
| 7JzXMWb7W4iAG2GRNmZZzHA7hTSKFvS7jc3SX3n6WvDtEvlOi8ay2RyRNgEjBDP6 | ||||
| VZ/w2jUJjS5k7dN0Qb9nhPr5B9fS/0CAppcVfsx5/KEVFzniWOPyzQYyW7FJKu8h | ||||
| 4G5hrp/Ie4UH5tKtB6YUZB/wliyyQUkAZdBcoy1hfkOZLAXb1oofArKsiQUHIRA5 | ||||
| th1yyS4cZP8Upngd1EE+d95dFHM2F6iI2lj6DHuu+JxUZ+wKXoNimdG7JniRtIf4 | ||||
| 56GoDov83Ey+XbIS6FSQc9nY0ijBDcubl/yP3roCQpE+MZ9BNEo5uj7YmCtAMYLW | ||||
| TXTNBGUCgYEA4wdkH1NLdub2NcpqwmSA0AtbRvDkt0XTDWWwmuMr/+xPVa4sUKHs | ||||
| 80THQEX/WAZroP6IPbMP6BJhzb53vECukgC65qPxu6M9D1lBGtglxgen4AMu1bKK | ||||
| gnM8onwARGIo/2ay6qRRZZCxg0TvBky3hbTcIM2zVrnKU6VVyGKHSV8CgYEAygxs | ||||
| WQYrACv3XN6ZEzyxy08JgjbcnkPWK/m3VPcyHgdEkDu8+nDdUVdbF/js2JWMMx5g | ||||
| vrPhZ7jVLOXGcLr5mVU4dG5tW5lU0bMy+YYxpEQDiBKlpXgfOsQnakHj7cCZ6bay | ||||
| mKjJck2oEAQS9bqOJN/Ts5vhOmc8rmhkO7hnAh8CgYEArhVDy9Vl/1WYo6SD+m1w | ||||
| bJbYtewPpQzwicxZAFuDqKk+KDf3GRkhBWTO2FUUOB4sN3YVaCI+5zf5MPeE/qAm | ||||
| fCP9LM+3k6bXMkbBamEljdTfACHQruJJ3T+Z1gn5dnZCc5z/QncfRx8NTtfz5MO8 | ||||
| 0dTeGnVAuBacs0kLHy2WCUcCgYALNBkl7pOf1NBIlAdE686oCV/rmoMtO3G6yoQB | ||||
| 8BsVUy3YGZfnAy8ifYeNkr3/XHuDsiGHMY5EJBmd/be9NID2oaUZv63MsHnljtw6 | ||||
| vdgu1Z6kgvQwcrK4nXvaBoFPA6kFLp5EnMde0TOKf89VVNzg6pBgmzon9OWGfj9g | ||||
| mF8N3QKBgQCeoLwxUxpzEA0CPHm7DWF0LefVGllgZ23Eqncdy0QRku5zwwibszbL | ||||
| sWaR3uDCc3oYcbSGCDVx3cSkvMAJNalc5ZHPfoV9W0+v392/rrExo5iwD8CSoCb2 | ||||
| gFWkeR7PBrD3NzFzFAWyiudzhBKHfRsB0MpCXbJV/WLqTlGIbEypjg== | ||||
| -----END RSA PRIVATE KEY----- | ||||
							
								
								
									
										
											BIN
										
									
								
								tests/static/python.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/static/python.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 11 KiB | 
| @@ -1,3 +1,4 @@ | ||||
| import asyncio | ||||
| import inspect | ||||
|  | ||||
| from sanic import Sanic | ||||
| @@ -23,6 +24,33 @@ def test_bp(): | ||||
|  | ||||
|     assert response.text == 'Hello' | ||||
|  | ||||
| def test_bp_strict_slash(): | ||||
|     app = Sanic('test_route_strict_slash') | ||||
|     bp = Blueprint('test_text') | ||||
|  | ||||
|     @bp.get('/get', strict_slashes=True) | ||||
|     def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     @bp.post('/post/', strict_slashes=True) | ||||
|     def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     app.blueprint(bp) | ||||
|  | ||||
|     request, response = app.test_client.get('/get') | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|     request, response = app.test_client.get('/get/') | ||||
|     assert response.status == 404 | ||||
|  | ||||
|     request, response = app.test_client.post('/post/') | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|     request, response = app.test_client.post('/post') | ||||
|     assert response.status == 404 | ||||
|  | ||||
|  | ||||
| def test_bp_with_url_prefix(): | ||||
|     app = Sanic('test_text') | ||||
|     bp = Blueprint('test_text', url_prefix='/test1') | ||||
| @@ -236,6 +264,7 @@ def test_bp_static(): | ||||
| def test_bp_shorthand(): | ||||
|     app = Sanic('test_shorhand_routes') | ||||
|     blueprint = Blueprint('test_shorhand_routes') | ||||
|     ev = asyncio.Event() | ||||
|  | ||||
|     @blueprint.get('/get') | ||||
|     def handler(request): | ||||
| @@ -265,6 +294,10 @@ def test_bp_shorthand(): | ||||
|     def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     @blueprint.websocket('/ws') | ||||
|     async def handler(request, ws): | ||||
|         ev.set() | ||||
|  | ||||
|     app.blueprint(blueprint) | ||||
|  | ||||
|     request, response = app.test_client.get('/get') | ||||
| @@ -308,3 +341,11 @@ def test_bp_shorthand(): | ||||
|  | ||||
|     request, response = app.test_client.get('/delete') | ||||
|     assert response.status == 405 | ||||
|  | ||||
|     request, response = app.test_client.get('/ws', headers={ | ||||
|         'Upgrade': 'websocket', | ||||
|         'Connection': 'upgrade', | ||||
|         'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', | ||||
|         'Sec-WebSocket-Version': '13'}) | ||||
|     assert response.status == 101 | ||||
|     assert ev.is_set() | ||||
|   | ||||
| @@ -16,6 +16,17 @@ def test_load_from_object(): | ||||
|     assert app.config.CONFIG_VALUE == 'should be used' | ||||
|     assert 'not_for_config' not in app.config | ||||
|  | ||||
| def test_auto_load_env(): | ||||
|     environ["SANIC_TEST_ANSWER"] = "42" | ||||
|     app = Sanic() | ||||
|     assert app.config.TEST_ANSWER == "42" | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
| def test_auto_load_env(): | ||||
|     environ["SANIC_TEST_ANSWER"] = "42" | ||||
|     app = Sanic(load_env=False) | ||||
|     assert getattr(app.config, 'TEST_ANSWER', None) == None | ||||
|     del environ["SANIC_TEST_ANSWER"] | ||||
|  | ||||
| def test_load_from_file(): | ||||
|     app = Sanic('test_load_from_file') | ||||
|   | ||||
| @@ -44,6 +44,21 @@ def exception_app(): | ||||
|  | ||||
|     return app | ||||
|  | ||||
| def test_catch_exception_list(): | ||||
|     app = Sanic('exception_list') | ||||
|     @app.exception([SanicExceptionTestException, NotFound]) | ||||
|     def exception_list(request, exception): | ||||
|         return text("ok") | ||||
|  | ||||
|     @app.route('/') | ||||
|     def exception(request): | ||||
|         raise SanicExceptionTestException("You won't see me") | ||||
|  | ||||
|     request, response = app.test_client.get('/random') | ||||
|     assert response.text == 'ok' | ||||
|  | ||||
|     request, response = app.test_client.get('/') | ||||
|     assert response.text == 'ok' | ||||
|  | ||||
| def test_no_exception(exception_app): | ||||
|     """Test that a route works without an exception""" | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import text | ||||
| from sanic.exceptions import InvalidUsage, ServerError, NotFound | ||||
| from sanic.handlers import ErrorHandler | ||||
| from bs4 import BeautifulSoup | ||||
|  | ||||
| exception_handler_app = Sanic('test_exception_handler') | ||||
| @@ -30,7 +31,7 @@ def handler_4(request): | ||||
| @exception_handler_app.route('/5') | ||||
| def handler_5(request): | ||||
|     class CustomServerError(ServerError): | ||||
|         status_code=200 | ||||
|         pass | ||||
|     raise CustomServerError('Custom server error') | ||||
|  | ||||
|  | ||||
| @@ -75,9 +76,50 @@ def test_html_traceback_output_in_debug_mode(): | ||||
|     summary_text = " ".join(soup.select('.summary')[0].text.split()) | ||||
|     assert ( | ||||
|         "NameError: name 'bar' " | ||||
|         "is not defined while handling uri /4") == summary_text | ||||
|         "is not defined while handling path /4") == summary_text | ||||
|  | ||||
|  | ||||
| def test_inherited_exception_handler(): | ||||
|     request, response = exception_handler_app.test_client.get('/5') | ||||
|     assert response.status == 200 | ||||
|  | ||||
|  | ||||
| def test_exception_handler_lookup(): | ||||
|     class CustomError(Exception): | ||||
|         pass | ||||
|  | ||||
|     class CustomServerError(ServerError): | ||||
|         pass | ||||
|  | ||||
|     def custom_error_handler(): | ||||
|         pass | ||||
|  | ||||
|     def server_error_handler(): | ||||
|         pass | ||||
|  | ||||
|     def import_error_handler(): | ||||
|         pass | ||||
|  | ||||
|     try: | ||||
|         ModuleNotFoundError | ||||
|     except: | ||||
|         class ModuleNotFoundError(ImportError): | ||||
|             pass | ||||
|  | ||||
|     handler = ErrorHandler() | ||||
|     handler.add(ImportError, import_error_handler) | ||||
|     handler.add(CustomError, custom_error_handler) | ||||
|     handler.add(ServerError, server_error_handler) | ||||
|  | ||||
|     assert handler.lookup(ImportError()) == import_error_handler | ||||
|     assert handler.lookup(ModuleNotFoundError()) == import_error_handler | ||||
|     assert handler.lookup(CustomError()) == custom_error_handler | ||||
|     assert handler.lookup(ServerError('Error')) == server_error_handler | ||||
|     assert handler.lookup(CustomServerError('Error')) == server_error_handler | ||||
|  | ||||
|     # once again to ensure there is no caching bug | ||||
|     assert handler.lookup(ImportError()) == import_error_handler | ||||
|     assert handler.lookup(ModuleNotFoundError()) == import_error_handler | ||||
|     assert handler.lookup(CustomError()) == custom_error_handler | ||||
|     assert handler.lookup(ServerError('Error')) == server_error_handler | ||||
|     assert handler.lookup(CustomServerError('Error')) == server_error_handler | ||||
|   | ||||
| @@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps | ||||
| from sanic import Sanic | ||||
| from sanic.request import Request | ||||
| from sanic.response import json, text, HTTPResponse | ||||
| from sanic.exceptions import NotFound | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| @@ -53,6 +54,27 @@ def test_middleware_response(): | ||||
|     assert isinstance(results[2], HTTPResponse) | ||||
|  | ||||
|  | ||||
| def test_middleware_response_exception(): | ||||
|     app = Sanic('test_middleware_response_exception') | ||||
|     result = {'status_code': None} | ||||
|  | ||||
|     @app.middleware('response') | ||||
|     async def process_response(reqest, response): | ||||
|         result['status_code'] = response.status | ||||
|         return response | ||||
|  | ||||
|     @app.exception(NotFound) | ||||
|     async def error_handler(request, exception): | ||||
|         return text('OK', exception.status_code) | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return text('FAIL') | ||||
|  | ||||
|     request, response = app.test_client.get('/page_not_found') | ||||
|     assert response.text == 'OK' | ||||
|     assert result['status_code'] == 404 | ||||
|  | ||||
| def test_middleware_override_request(): | ||||
|     app = Sanic('test_middleware_override_request') | ||||
|  | ||||
|   | ||||
| @@ -1,49 +1,47 @@ | ||||
| from sanic import Sanic | ||||
| from sanic.response import text | ||||
| from sanic.exceptions import PayloadTooLarge | ||||
| from sanic.response import text | ||||
|  | ||||
|  | ||||
| def test_payload_too_large_from_error_handler(): | ||||
|     data_received_app = Sanic('data_received') | ||||
|     data_received_app.config.REQUEST_MAX_SIZE = 1 | ||||
| data_received_default_app = Sanic('data_received_default') | ||||
| data_received_default_app.config.REQUEST_MAX_SIZE = 1 | ||||
| on_header_default_app = Sanic('on_header') | ||||
| on_header_default_app.config.REQUEST_MAX_SIZE = 500 | ||||
|  | ||||
|  | ||||
|     @data_received_app.route('/1') | ||||
|     async def handler1(request): | ||||
|         return text('OK') | ||||
|  | ||||
|  | ||||
|     @data_received_app.exception(PayloadTooLarge) | ||||
|     def handler_exception(request, exception): | ||||
|         return text('Payload Too Large from error_handler.', 413) | ||||
|  | ||||
|  | ||||
| def test_payload_too_large_from_error_handler(): | ||||
|     response = data_received_app.test_client.get('/1', gather_request=False) | ||||
|     assert response.status == 413 | ||||
|     assert response.text == 'Payload Too Large from error_handler.' | ||||
|  | ||||
|  | ||||
| def test_payload_too_large_at_data_received_default(): | ||||
|     data_received_default_app = Sanic('data_received_default') | ||||
|     data_received_default_app.config.REQUEST_MAX_SIZE = 1 | ||||
|  | ||||
|     @data_received_default_app.route('/1') | ||||
|     async def handler2(request): | ||||
|         return text('OK') | ||||
|  | ||||
|  | ||||
| def test_payload_too_large_at_data_received_default(): | ||||
|     response = data_received_default_app.test_client.get( | ||||
|         '/1', gather_request=False) | ||||
|     assert response.status == 413 | ||||
|     assert response.text == 'Error: Payload Too Large' | ||||
|  | ||||
|  | ||||
| @on_header_default_app.route('/1') | ||||
| def test_payload_too_large_at_on_header_default(): | ||||
|     on_header_default_app = Sanic('on_header') | ||||
|     on_header_default_app.config.REQUEST_MAX_SIZE = 500 | ||||
|  | ||||
|     @on_header_default_app.post('/1') | ||||
|     async def handler3(request): | ||||
|         return text('OK') | ||||
|  | ||||
|  | ||||
| def test_payload_too_large_at_on_header_default(): | ||||
|     data = 'a' * 1000 | ||||
|     response = on_header_default_app.test_client.post( | ||||
|         '/1', gather_request=False, data=data) | ||||
|   | ||||
| @@ -88,4 +88,7 @@ def test_chained_redirect(redirect_app): | ||||
|     assert request.url.endswith('/1') | ||||
|     assert response.status == 200 | ||||
|     assert response.text == 'OK' | ||||
|     try: | ||||
|         assert response.url.endswith('/3') | ||||
|     except AttributeError: | ||||
|         assert response.url.path.endswith('/3') | ||||
|   | ||||
| @@ -1,10 +1,15 @@ | ||||
| from json import loads as json_loads, dumps as json_dumps | ||||
| from urllib.parse import urlparse | ||||
| import os | ||||
| import ssl | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.exceptions import ServerError | ||||
| from sanic.response import json, text, redirect | ||||
| from sanic.response import json, text | ||||
|  | ||||
| from sanic.testing import HOST, PORT | ||||
|  | ||||
|  | ||||
| # ------------------------------------------------------------ # | ||||
| @@ -85,20 +90,28 @@ def test_json(): | ||||
|  | ||||
|     request, response = app.test_client.get('/') | ||||
|  | ||||
|     try: | ||||
|     results = json_loads(response.text) | ||||
|     except: | ||||
|         raise ValueError("Expected JSON response but got '{}'".format(response)) | ||||
|  | ||||
|     assert results.get('test') == True | ||||
|  | ||||
| def test_empty_json(): | ||||
|     app = Sanic('test_json') | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         assert request.json == None | ||||
|         return json(request.json) | ||||
|  | ||||
|     request, response = app.test_client.get('/') | ||||
|     assert response.status == 200 | ||||
|     assert response.text == 'null' | ||||
|  | ||||
| def test_invalid_json(): | ||||
|     app = Sanic('test_json') | ||||
|  | ||||
|     @app.route('/') | ||||
|     async def handler(request): | ||||
|         return json(request.json()) | ||||
|         return json(request.json) | ||||
|  | ||||
|     data = "I am not json" | ||||
|     request, response = app.test_client.get('/', data=data) | ||||
| @@ -128,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', | ||||
| @@ -138,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 | ||||
| # ------------------------------------------------------------ # | ||||
| @@ -192,3 +227,61 @@ def test_post_form_multipart_form_data(): | ||||
|     request, response = app.test_client.post(data=payload, headers=headers) | ||||
|  | ||||
|     assert request.form.get('test') == 'OK' | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     'path,query,expected_url', [ | ||||
|         ('/foo', '', 'http://{}:{}/foo'), | ||||
|         ('/bar/baz', '', 'http://{}:{}/bar/baz'), | ||||
|         ('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1') | ||||
|     ]) | ||||
| def test_url_attributes_no_ssl(path, query, expected_url): | ||||
|     app = Sanic('test_url_attrs_no_ssl') | ||||
|  | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     app.add_route(handler, path) | ||||
|  | ||||
|     request, response = app.test_client.get(path + '?{}'.format(query)) | ||||
|     assert request.url == expected_url.format(HOST, PORT) | ||||
|  | ||||
|     parsed = urlparse(request.url) | ||||
|  | ||||
|     assert parsed.scheme == request.scheme | ||||
|     assert parsed.path == request.path | ||||
|     assert parsed.query == request.query_string | ||||
|     assert parsed.netloc == request.host | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize( | ||||
|     'path,query,expected_url', [ | ||||
|         ('/foo', '', 'https://{}:{}/foo'), | ||||
|         ('/bar/baz', '', 'https://{}:{}/bar/baz'), | ||||
|         ('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1') | ||||
|     ]) | ||||
| def test_url_attributes_with_ssl(path, query, expected_url): | ||||
|     app = Sanic('test_url_attrs_with_ssl') | ||||
|  | ||||
|     current_dir = os.path.dirname(os.path.realpath(__file__)) | ||||
|     context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) | ||||
|     context.load_cert_chain( | ||||
|         os.path.join(current_dir, 'certs/selfsigned.cert'), | ||||
|         keyfile=os.path.join(current_dir, 'certs/selfsigned.key')) | ||||
|  | ||||
|     async def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     app.add_route(handler, path) | ||||
|  | ||||
|     request, response = app.test_client.get( | ||||
|         'https://{}:{}'.format(HOST, PORT) + path + '?{}'.format(query), | ||||
|         server_kwargs={'ssl': context}) | ||||
|     assert request.url == expected_url.format(HOST, PORT) | ||||
|  | ||||
|     parsed = urlparse(request.url) | ||||
|  | ||||
|     assert parsed.scheme == request.scheme | ||||
|     assert parsed.path == request.path | ||||
|     assert parsed.query == request.query_string | ||||
|     assert parsed.netloc == request.host | ||||
|   | ||||
| @@ -1,8 +1,12 @@ | ||||
| import asyncio | ||||
| import pytest | ||||
| from random import choice | ||||
|  | ||||
| from sanic import Sanic | ||||
| from sanic.response import HTTPResponse | ||||
| from sanic.response import HTTPResponse, stream, StreamingHTTPResponse | ||||
| from sanic.testing import HOST, PORT | ||||
|  | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| def test_response_body_not_a_string(): | ||||
|     """Test when a response body sent from the application is not a string""" | ||||
| @@ -15,3 +19,78 @@ def test_response_body_not_a_string(): | ||||
|  | ||||
|     request, response = app.test_client.get('/hello') | ||||
|     assert response.text == str(random_num) | ||||
|  | ||||
|  | ||||
| async def sample_streaming_fn(response): | ||||
|     response.write('foo,') | ||||
|     await asyncio.sleep(.001) | ||||
|     response.write('bar') | ||||
|  | ||||
|  | ||||
| @pytest.fixture | ||||
| def streaming_app(): | ||||
|     app = Sanic('streaming') | ||||
|  | ||||
|     @app.route("/") | ||||
|     async def test(request): | ||||
|         return stream(sample_streaming_fn, content_type='text/csv') | ||||
|  | ||||
|     return app | ||||
|  | ||||
|  | ||||
| def test_streaming_adds_correct_headers(streaming_app): | ||||
|     request, response = streaming_app.test_client.get('/') | ||||
|     assert response.headers['Transfer-Encoding'] == 'chunked' | ||||
|     assert response.headers['Content-Type'] == 'text/csv' | ||||
|  | ||||
|  | ||||
| def test_streaming_returns_correct_content(streaming_app): | ||||
|     request, response = streaming_app.test_client.get('/') | ||||
|     assert response.text == 'foo,bar' | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('status', [200, 201, 400, 401]) | ||||
| def test_stream_response_status_returns_correct_headers(status): | ||||
|     response = StreamingHTTPResponse(sample_streaming_fn, status=status) | ||||
|     headers = response.get_headers() | ||||
|     assert b"HTTP/1.1 %s" % str(status).encode() in headers | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('keep_alive_timeout', [10, 20, 30]) | ||||
| def test_stream_response_keep_alive_returns_correct_headers( | ||||
|         keep_alive_timeout): | ||||
|     response = StreamingHTTPResponse(sample_streaming_fn) | ||||
|     headers = response.get_headers( | ||||
|         keep_alive=True, keep_alive_timeout=keep_alive_timeout) | ||||
|  | ||||
|     assert b"Keep-Alive: %s\r\n" % str(keep_alive_timeout).encode() in headers | ||||
|  | ||||
|  | ||||
| def test_stream_response_includes_chunked_header(): | ||||
|     response = StreamingHTTPResponse(sample_streaming_fn) | ||||
|     headers = response.get_headers() | ||||
|     assert b"Transfer-Encoding: chunked\r\n" in headers | ||||
|  | ||||
|  | ||||
| def test_stream_response_writes_correct_content_to_transport(streaming_app): | ||||
|     response = StreamingHTTPResponse(sample_streaming_fn) | ||||
|     response.transport = MagicMock(asyncio.Transport) | ||||
|  | ||||
|     @streaming_app.listener('after_server_start') | ||||
|     async def run_stream(app, loop): | ||||
|         await response.stream() | ||||
|         assert response.transport.write.call_args_list[1][0][0] == ( | ||||
|             b'4\r\nfoo,\r\n' | ||||
|         ) | ||||
|  | ||||
|         assert response.transport.write.call_args_list[2][0][0] == ( | ||||
|             b'3\r\nbar\r\n' | ||||
|         ) | ||||
|  | ||||
|         assert response.transport.write.call_args_list[3][0][0] == ( | ||||
|             b'0\r\n\r\n' | ||||
|         ) | ||||
|  | ||||
|         app.stop() | ||||
|  | ||||
|     streaming_app.run(host=HOST, port=PORT) | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import asyncio | ||||
| import pytest | ||||
|  | ||||
| from sanic import Sanic | ||||
| @@ -22,6 +23,29 @@ def test_shorthand_routes_get(): | ||||
|     request, response = app.test_client.post('/get') | ||||
|     assert response.status == 405 | ||||
|  | ||||
| def test_route_strict_slash(): | ||||
|     app = Sanic('test_route_strict_slash') | ||||
|  | ||||
|     @app.get('/get', strict_slashes=True) | ||||
|     def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     @app.post('/post/', strict_slashes=True) | ||||
|     def handler(request): | ||||
|         return text('OK') | ||||
|  | ||||
|     request, response = app.test_client.get('/get') | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|     request, response = app.test_client.get('/get/') | ||||
|     assert response.status == 404 | ||||
|  | ||||
|     request, response = app.test_client.post('/post/') | ||||
|     assert response.text == 'OK' | ||||
|  | ||||
|     request, response = app.test_client.post('/post') | ||||
|     assert response.status == 404 | ||||
|  | ||||
| def test_route_optional_slash(): | ||||
|     app = Sanic('test_route_optional_slash') | ||||
|  | ||||
| @@ -214,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') | ||||
|  | ||||
| @@ -234,6 +282,23 @@ def test_dynamic_route_unhashable(): | ||||
|     assert response.status == 404 | ||||
|  | ||||
|  | ||||
| def test_websocket_route(): | ||||
|     app = Sanic('test_websocket_route') | ||||
|     ev = asyncio.Event() | ||||
|  | ||||
|     @app.websocket('/ws') | ||||
|     async def handler(request, ws): | ||||
|         ev.set() | ||||
|  | ||||
|     request, response = app.test_client.get('/ws', headers={ | ||||
|         'Upgrade': 'websocket', | ||||
|         'Connection': 'upgrade', | ||||
|         'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', | ||||
|         'Sec-WebSocket-Version': '13'}) | ||||
|     assert response.status == 101 | ||||
|     assert ev.is_set() | ||||
|  | ||||
|  | ||||
| def test_route_duplicate(): | ||||
|     app = Sanic('test_route_duplicate') | ||||
|  | ||||
|   | ||||
| @@ -25,7 +25,7 @@ def get_file_content(static_file_directory, file_name): | ||||
|         return file.read() | ||||
|  | ||||
|  | ||||
| @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) | ||||
| @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) | ||||
| def test_static_file(static_file_directory, file_name): | ||||
|     app = Sanic('test_static') | ||||
|     app.static( | ||||
|   | ||||
| @@ -15,13 +15,13 @@ def test_methods(method): | ||||
|  | ||||
|     class DummyView(HTTPMethodView): | ||||
|  | ||||
|         def get(self, request): | ||||
|         async def get(self, request): | ||||
|             return text('', headers={'method': 'GET'}) | ||||
|  | ||||
|         def post(self, request): | ||||
|             return text('', headers={'method': 'POST'}) | ||||
|  | ||||
|         def put(self, request): | ||||
|         async def put(self, request): | ||||
|             return text('', headers={'method': 'PUT'}) | ||||
|  | ||||
|         def head(self, request): | ||||
| @@ -30,7 +30,7 @@ def test_methods(method): | ||||
|         def options(self, request): | ||||
|             return text('', headers={'method': 'OPTIONS'}) | ||||
|  | ||||
|         def patch(self, request): | ||||
|         async def patch(self, request): | ||||
|             return text('', headers={'method': 'PATCH'}) | ||||
|  | ||||
|         def delete(self, request): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user