Compare commits
	
		
			218 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 18829e648a | ||
|   | a64c636a33 | ||
|   | 5796f211c1 | ||
|   | ae09dec05e | ||
|   | afd51e0823 | ||
|   | 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 | ||
|   | 5e5f513088 | ||
|   | 9fcf725061 | ||
|   | 601e015f00 | ||
|   | 3289e8403a | ||
|   | 104a7c7d05 | ||
|   | 7560660ec7 | ||
|   | 40ccb4a0dd | ||
|   | f90288f5dc | ||
|   | 3bf79898d9 | ||
|   | 1d6e11ca10 | ||
|   | 6e903ee7d5 | ||
|   | 21fb1dff7e | ||
|   | da924a359c | ||
|   | a5066f15dc | ||
|   | 2dca53a696 | ||
|   | d8a6d7e02f | ||
|   | 1a8961587c | ||
|   | fa13ad8849 | ||
|   | 8b23dec322 | ||
|   | 4e8aac4b41 | ||
|   | e6a828572a | ||
|   | 6ea43d8e6d | 
							
								
								
									
										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`` | -  ``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 | Documentation | ||||||
| ------------- | ------------- | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								docs/conf.py
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								docs/conf.py
									
									
									
									
									
								
							| @@ -22,7 +22,7 @@ import sanic | |||||||
|  |  | ||||||
| # -- General configuration ------------------------------------------------ | # -- General configuration ------------------------------------------------ | ||||||
|  |  | ||||||
| extensions = [] | extensions = ['sphinx.ext.autodoc'] | ||||||
|  |  | ||||||
| templates_path = ['_templates'] | templates_path = ['_templates'] | ||||||
|  |  | ||||||
| @@ -68,7 +68,6 @@ pygments_style = 'sphinx' | |||||||
| # If true, `todo` and `todoList` produce output, else they produce nothing. | # If true, `todo` and `todoList` produce output, else they produce nothing. | ||||||
| todo_include_todos = False | todo_include_todos = False | ||||||
|  |  | ||||||
|  |  | ||||||
| # -- Options for HTML output ---------------------------------------------- | # -- Options for HTML output ---------------------------------------------- | ||||||
|  |  | ||||||
| # The theme to use for HTML and HTML Help pages.  See the documentation for | # 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". | # so a file named "default.css" will overwrite the builtin "default.css". | ||||||
| html_static_path = ['_static'] | html_static_path = ['_static'] | ||||||
|  |  | ||||||
|  |  | ||||||
| # -- Options for HTMLHelp output ------------------------------------------ | # -- Options for HTMLHelp output ------------------------------------------ | ||||||
|  |  | ||||||
| # Output file base name for HTML help builder. | # Output file base name for HTML help builder. | ||||||
| htmlhelp_basename = 'Sanicdoc' | htmlhelp_basename = 'Sanicdoc' | ||||||
|  |  | ||||||
|  |  | ||||||
| # -- Options for LaTeX output --------------------------------------------- | # -- Options for LaTeX output --------------------------------------------- | ||||||
|  |  | ||||||
| latex_elements = { | latex_elements = { | ||||||
| @@ -110,21 +107,14 @@ latex_elements = { | |||||||
| # Grouping the document tree into LaTeX files. List of tuples | # Grouping the document tree into LaTeX files. List of tuples | ||||||
| # (source start file, target name, title, | # (source start file, target name, title, | ||||||
| #  author, documentclass [howto, manual, or own class]). | #  author, documentclass [howto, manual, or own class]). | ||||||
| latex_documents = [ | latex_documents = [(master_doc, 'Sanic.tex', 'Sanic Documentation', | ||||||
|     (master_doc, 'Sanic.tex', 'Sanic Documentation', |                     'Sanic contributors', 'manual'), ] | ||||||
|      'Sanic contributors', 'manual'), |  | ||||||
| ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # -- Options for manual page output --------------------------------------- | # -- Options for manual page output --------------------------------------- | ||||||
|  |  | ||||||
| # One entry per manual page. List of tuples | # One entry per manual page. List of tuples | ||||||
| # (source start file, name, description, authors, manual section). | # (source start file, name, description, authors, manual section). | ||||||
| man_pages = [ | man_pages = [(master_doc, 'sanic', 'Sanic Documentation', [author], 1)] | ||||||
|     (master_doc, 'sanic', 'Sanic Documentation', |  | ||||||
|      [author], 1) |  | ||||||
| ] |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # -- Options for Texinfo output ------------------------------------------- | # -- Options for Texinfo output ------------------------------------------- | ||||||
|  |  | ||||||
| @@ -132,13 +122,10 @@ man_pages = [ | |||||||
| # (source start file, target name, title, author, | # (source start file, target name, title, author, | ||||||
| #  dir menu entry, description, category) | #  dir menu entry, description, category) | ||||||
| texinfo_documents = [ | texinfo_documents = [ | ||||||
|     (master_doc, 'Sanic', 'Sanic Documentation', |     (master_doc, 'Sanic', 'Sanic Documentation', author, 'Sanic', | ||||||
|      author, 'Sanic', 'One line description of project.', |      'One line description of project.', 'Miscellaneous'), | ||||||
|      'Miscellaneous'), |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # -- Options for Epub output ---------------------------------------------- | # -- Options for Epub output ---------------------------------------------- | ||||||
|  |  | ||||||
| # Bibliographic Dublin Core info. | # Bibliographic Dublin Core info. | ||||||
| @@ -150,8 +137,6 @@ epub_copyright = copyright | |||||||
| # A list of files that should not be packed into the epub file. | # A list of files that should not be packed into the epub file. | ||||||
| epub_exclude_files = ['search.html'] | epub_exclude_files = ['search.html'] | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # -- Custom Settings ------------------------------------------------------- | # -- Custom Settings ------------------------------------------------------- | ||||||
|  |  | ||||||
| suppress_warnings = ['image.nonlocal_uri'] | suppress_warnings = ['image.nonlocal_uri'] | ||||||
|   | |||||||
| @@ -9,12 +9,14 @@ Guides | |||||||
|    sanic/getting_started |    sanic/getting_started | ||||||
|    sanic/routing |    sanic/routing | ||||||
|    sanic/request_data |    sanic/request_data | ||||||
|  |    sanic/response | ||||||
|    sanic/static_files |    sanic/static_files | ||||||
|    sanic/exceptions |    sanic/exceptions | ||||||
|    sanic/middleware |    sanic/middleware | ||||||
|    sanic/blueprints |    sanic/blueprints | ||||||
|    sanic/config |    sanic/config | ||||||
|    sanic/cookies |    sanic/cookies | ||||||
|  |    sanic/streaming | ||||||
|    sanic/class_based_views |    sanic/class_based_views | ||||||
|    sanic/custom_protocol |    sanic/custom_protocol | ||||||
|    sanic/ssl |    sanic/ssl | ||||||
|   | |||||||
| @@ -55,13 +55,18 @@ will look like: | |||||||
|  |  | ||||||
| Blueprints have much the same functionality as an application instance. | 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 | ### Middleware | ||||||
|  |  | ||||||
| Using blueprints allows you to also register middleware globally. | Using blueprints allows you to also register middleware globally. | ||||||
|  |  | ||||||
| ```python | ```python | ||||||
| @bp.middleware | @bp.middleware | ||||||
| async def halt_request(request): | async def print_on_request(request): | ||||||
| 	print("I am a spy") | 	print("I am a spy") | ||||||
|  |  | ||||||
| @bp.middleware('request') | @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 | ## URL parameters | ||||||
|  |  | ||||||
| If you need any URL parameters, as discussed in the routing guide, include them | 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. | 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 | ### 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: | If there are a lot of configuration values and they have sensible defaults it might be helpful to put them into a module: | ||||||
| @@ -71,8 +79,7 @@ DB_USER = 'appuser' | |||||||
| 
 | 
 | ||||||
| Out of the box there are just a few predefined values which can be overwritten when creating the application. | Out of the box there are just a few predefined values which can be overwritten when creating the application. | ||||||
| 
 | 
 | ||||||
| | Variable          | Default   | Description                       | |     | Variable          | Default   | Description                       | | ||||||
| | ----------------- | --------- | --------------------------------- | |     | ----------------- | --------- | --------------------------------- | | ||||||
| | REQUEST_MAX_SIZE  | 100000000 | How big a request may be (bytes)  | |     | REQUEST_MAX_SIZE  | 100000000 | How big a request may be (bytes)  | | ||||||
| | REQUEST_TIMEOUT   | 60        | How long a request can take (sec) | |     | REQUEST_TIMEOUT   | 60        | How long a request can take (sec) | | ||||||
| 
 |  | ||||||
| @@ -5,7 +5,7 @@ both read and write cookies, which are stored as key-value pairs. | |||||||
|  |  | ||||||
| ## Reading cookies | ## 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 | ```python | ||||||
| from sanic.response import text | 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__': | if __name__ == '__main__': | ||||||
|     app.run(host='0.0.0.0', port=1337, workers=4) |     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. | - [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. | - [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 | - [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 is developed `on GitHub <https://github.com/channelcat/sanic/>`_. Contributions are welcome! | ||||||
|  |  | ||||||
| Sanic aspires to be simple: | Sanic aspires to be simple | ||||||
| ------------------- | --------------------------- | ||||||
|  |  | ||||||
| .. code:: python | .. code:: python | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,13 @@ | |||||||
| # Middleware | # Middleware And Listeners | ||||||
|  |  | ||||||
| Middleware are functions which are executed before or after requests to the | 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* | server. They can be used to modify the *request to* or *response from* | ||||||
| user-defined handler functions. | 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 | There are two types of middleware: request and response. Both are declared | ||||||
| using the `@app.middleware` decorator, with the decorator's parameter being a | using the `@app.middleware` decorator, with the decorator's parameter being a | ||||||
| string representing its type: `'request'` or `'response'`. Response middleware | string representing its type: `'request'` or `'response'`. Response middleware | ||||||
| @@ -64,3 +68,45 @@ async def halt_request(request): | |||||||
| async def halt_response(request, response): | async def halt_response(request, response): | ||||||
| 	return text('I halted the 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 | - `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, |   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. |   The request's `query_string` variable holds the unparsed string value. | ||||||
|  |  | ||||||
|   ```python |   ```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 }) |       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 | - `files` (dictionary of `File` objects) - List of files that have a name, body, and type | ||||||
|  |  | ||||||
|   ```python |   ```python | ||||||
| @@ -85,6 +89,12 @@ The following variables are accessible as properties on `Request` objects: | |||||||
|           return json({'status': 'production'}) |           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` | ## 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 | # 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. | - 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") |   context.load_cert_chain("/path/to/cert", keyfile="/path/to/keyfile") | ||||||
|  |  | ||||||
|   app.run(host="0.0.0.0", port=8443, ssl=context) |   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 |   - httptools>=0.0.9 | ||||||
|   - ujson>=1.35 |   - ujson>=1.35 | ||||||
|   - aiofiles>=0.3.0 |   - aiofiles>=0.3.0 | ||||||
|  |   - websockets>=3.2 | ||||||
|   - https://github.com/channelcat/docutils-fork/zipball/master |   - https://github.com/channelcat/docutils-fork/zipball/master | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic import response | ||||||
|  |  | ||||||
| import aiohttp | import aiohttp | ||||||
|  |  | ||||||
| @@ -9,20 +9,18 @@ async def fetch(session, url): | |||||||
|     """ |     """ | ||||||
|     Use session object to perform 'get' request on url |     Use session object to perform 'get' request on url | ||||||
|     """ |     """ | ||||||
|     async with session.get(url) as response: |     async with session.get(url) as result: | ||||||
|         return await response.json() |         return await result.json() | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route('/') | ||||||
| async def test(request): | async def handle_request(request): | ||||||
|     """ |  | ||||||
|     Download and serve example JSON |  | ||||||
|     """ |  | ||||||
|     url = "https://api.github.com/repos/channelcat/sanic" |     url = "https://api.github.com/repos/channelcat/sanic" | ||||||
|      |      | ||||||
|     async with aiohttp.ClientSession() as session: |     async with aiohttp.ClientSession() as session: | ||||||
|         response = await fetch(session, url) |         result = await fetch(session, url) | ||||||
|         return json(response) |         return response.json(result) | ||||||
|  |  | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000, workers=2) | 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__) | app = Sanic(__name__) | ||||||
| blueprint = Blueprint('name', url_prefix='/my_blueprint') | blueprint = Blueprint('name', url_prefix='/my_blueprint') | ||||||
| blueprint2 = Blueprint('name', url_prefix='/my_blueprint2') | blueprint2 = Blueprint('name', url_prefix='/my_blueprint2') | ||||||
|  | blueprint3 = Blueprint('name', url_prefix='/my_blueprint3') | ||||||
|  |  | ||||||
|  |  | ||||||
| @blueprint.route('/foo') | @blueprint.route('/foo') | ||||||
| @@ -17,8 +18,17 @@ async def foo(request): | |||||||
| async def foo2(request): | async def foo2(request): | ||||||
|     return json({'msg': 'hi from blueprint2'}) |     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.blueprint(blueprint) | ||||||
| app.register_blueprint(blueprint2) | app.blueprint(blueprint2) | ||||||
|  | app.blueprint(blueprint3) | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000, debug=True) | app.run(host="0.0.0.0", port=8000, debug=True) | ||||||
|   | |||||||
							
								
								
									
										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. | Example intercepting uncaught exceptions using Sanic's error handler framework. | ||||||
|  |  | ||||||
| This may be useful for developers wishing to use Sentry, Airbrake, etc. | This may be useful for developers wishing to use Sentry, Airbrake, etc. | ||||||
| or a custom system to log and monitor unexpected errors in production. | or a custom system to log and monitor unexpected errors in production. | ||||||
|  |  | ||||||
| First we create our own class inheriting from Handler in sanic.exceptions, | 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 | 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 | class' default handler, we can do anything including sending exceptions to | ||||||
| an external service. | 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 | Imports and code relevant for our CustomHandler class | ||||||
| (Ordinarily this would be in a separate file) | (Ordinarily this would be in a separate file) | ||||||
| """ | """ | ||||||
|  |  | ||||||
|  |  | ||||||
| class CustomHandler(Handler): | class CustomHandler(ErrorHandler): | ||||||
|  |  | ||||||
|     def default(self, request, exception): |     def default(self, request, exception): | ||||||
|         # Here, we have access to the exception object |         # 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 import Sanic | ||||||
| from sanic.response import json | from sanic import response | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic(__name__) | ||||||
|  |  | ||||||
| handler = CustomHandler(sanic=app) | handler = CustomHandler() | ||||||
| app.error_handler = handler | app.error_handler = handler | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -51,7 +50,7 @@ async def test(request): | |||||||
|     # Here, something occurs which causes an unexpected exception |     # Here, something occurs which causes an unexpected exception | ||||||
|     # This exception will flow to our custom handler. |     # This exception will flow to our custom handler. | ||||||
|     1 / 0 |     1 / 0 | ||||||
|     return json({"test": True}) |     return response.json({"test": True}) | ||||||
|  |  | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000, debug=True) | app.run(host="0.0.0.0", port=8000, debug=True) | ||||||
| @@ -2,7 +2,7 @@ | |||||||
| # curl -d '{"name": "John Doe"}' localhost:8000 | # curl -d '{"name": "John Doe"}' localhost:8000 | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import html | from sanic import response | ||||||
| from jinja2 import Template | from jinja2 import Template | ||||||
|  |  | ||||||
| template = Template('Hello {{ name }}!') | template = Template('Hello {{ name }}!') | ||||||
| @@ -12,7 +12,7 @@ app = Sanic(__name__) | |||||||
| @app.route('/') | @app.route('/') | ||||||
| async def test(request): | async def test(request): | ||||||
|     data = request.json |     data = request.json | ||||||
|     return html(template.render(**data)) |     return response.html(template.render(**data)) | ||||||
|  |  | ||||||
|  |  | ||||||
| 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 import Sanic | ||||||
| from sanic.response import text | from sanic import response | ||||||
| import json |  | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| logging_format = "[%(asctime)s] %(process)d-%(levelname)s " | logging_format = "[%(asctime)s] %(process)d-%(levelname)s " | ||||||
| @@ -18,6 +17,6 @@ sanic = Sanic() | |||||||
| @sanic.route("/") | @sanic.route("/") | ||||||
| def test(request): | def test(request): | ||||||
|     log.info("received request; responding with 'hey'") |     log.info("received request; responding with 'hey'") | ||||||
|     return text("hey") |     return response.text("hey") | ||||||
|  |  | ||||||
| sanic.run(host="0.0.0.0", port=8000) | 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 | import asyncio | ||||||
| from sanic.response import text | from sanic import Sanic | ||||||
|  | from sanic import response | ||||||
| from sanic.config import Config | from sanic.config import Config | ||||||
| from sanic.exceptions import RequestTimeout | from sanic.exceptions import RequestTimeout | ||||||
|  |  | ||||||
| @@ -11,11 +11,11 @@ app = Sanic(__name__) | |||||||
| @app.route('/') | @app.route('/') | ||||||
| async def test(request): | async def test(request): | ||||||
|     await asyncio.sleep(3) |     await asyncio.sleep(3) | ||||||
|     return text('Hello, world!') |     return response.text('Hello, world!') | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.exception(RequestTimeout) | @app.exception(RequestTimeout) | ||||||
| def timeout(request, exception): | 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) | app.run(host='0.0.0.0', port=8000) | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic import response | ||||||
| from multiprocessing import Event | from multiprocessing import Event | ||||||
| from signal import signal, SIGINT | from signal import signal, SIGINT | ||||||
| import asyncio | import asyncio | ||||||
| @@ -9,10 +9,10 @@ app = Sanic(__name__) | |||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
| async def test(request): | async def test(request): | ||||||
|     return json({"answer": "42"}) |     return response.json({"answer": "42"}) | ||||||
|  |  | ||||||
| asyncio.set_event_loop(uvloop.new_event_loop()) | 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() | loop = asyncio.get_event_loop() | ||||||
| task = asyncio.ensure_future(server) | task = asyncio.ensure_future(server) | ||||||
| signal(SIGINT, lambda s, f: loop.stop()) | 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 os | ||||||
| import asyncio | import asyncio | ||||||
|  |  | ||||||
| import uvloop | import uvloop | ||||||
| from asyncpg import create_pool | from asyncpg import connect, create_pool | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic.response import json | ||||||
|  |  | ||||||
| DB_CONFIG = { | DB_CONFIG = { | ||||||
|     'host': '<host>', |     'host': '<host>', | ||||||
|     'user': '<username>', |     'user': '<user>', | ||||||
|     'password': '<password>', |     'password': '<password>', | ||||||
|     'port': '<port>', |     'port': '<port>', | ||||||
|     'database': '<database>' |     'database': '<database>' | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| def jsonify(records): | def jsonify(records): | ||||||
|     """ |     """ | ||||||
|     Parse asyncpg record response into JSON format |     Parse asyncpg record response into JSON format | ||||||
|     """ |     """ | ||||||
|     return [{key: value for key, value in |     return [dict(r.items()) for r in records] | ||||||
|              zip(r.keys(), r.values())} for r in records] |  | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.listener('before_server_start') | @app.listener('before_server_start') | ||||||
| async def create_db(app, loop): | async def register_db(app, loop): | ||||||
|     """ |     app.pool = await create_pool(**DB_CONFIG, loop=loop, max_size=100) | ||||||
|     Create some table and add some data |     async with app.pool.acquire() as connection: | ||||||
|     """ |  | ||||||
|     async with create_pool(**DB_CONFIG) as pool: |  | ||||||
|         async with pool.acquire() as connection: |  | ||||||
|             async with connection.transaction(): |  | ||||||
|         await connection.execute('DROP TABLE IF EXISTS sanic_post') |         await connection.execute('DROP TABLE IF EXISTS sanic_post') | ||||||
|         await connection.execute("""CREATE TABLE sanic_post ( |         await connection.execute("""CREATE TABLE sanic_post ( | ||||||
|                                 id serial primary key, |                                 id serial primary key, | ||||||
|                                 content varchar(50), |                                 content varchar(50), | ||||||
|                                 post_date timestamp |                                 post_date timestamp | ||||||
|                             );""") |                             );""") | ||||||
|                 for i in range(0, 100): |         for i in range(0, 1000): | ||||||
|             await connection.execute(f"""INSERT INTO sanic_post |             await connection.execute(f"""INSERT INTO sanic_post | ||||||
|                 (id, content, post_date) VALUES ({i}, {i}, now())""") |                 (id, content, post_date) VALUES ({i}, {i}, now())""") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.get('/') | ||||||
| async def handler(request): | async def root_get(request): | ||||||
|     async with create_pool(**DB_CONFIG) as pool: |     async with app.pool.acquire() as connection: | ||||||
|         async with pool.acquire() as connection: |  | ||||||
|             async with connection.transaction(): |  | ||||||
|         results = await connection.fetch('SELECT * FROM sanic_post') |         results = await connection.fetch('SELECT * FROM sanic_post') | ||||||
|         return json({'posts': jsonify(results)}) |         return json({'posts': jsonify(results)}) | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': | 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 | sanic==0.2.0 | ||||||
| """ | """ | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic import response | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic('motor_mongodb') | app = Sanic('motor_mongodb') | ||||||
| @@ -25,7 +25,7 @@ async def get(request): | |||||||
|     for doc in docs: |     for doc in docs: | ||||||
|         doc['id'] = str(doc['_id']) |         doc['id'] = str(doc['_id']) | ||||||
|         del doc['_id'] |         del doc['_id'] | ||||||
|     return json(docs) |     return response.json(docs) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route('/post', methods=['POST']) | @app.route('/post', methods=['POST']) | ||||||
| @@ -34,8 +34,8 @@ async def new(request): | |||||||
|     print(doc) |     print(doc) | ||||||
|     db = get_db() |     db = get_db() | ||||||
|     object_id = await db.test_col.save(doc) |     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__": | 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 | ## You need the following additional packages for this example | ||||||
| # aiopg | # aiopg | ||||||
| # peewee_async | # peewee_async | ||||||
| @@ -10,8 +11,9 @@ from sanic.response import json | |||||||
|  |  | ||||||
| ## peewee_async related imports | ## peewee_async related imports | ||||||
| import peewee | 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 |  # we instantiate a custom loop so we can pass it to our db manager | ||||||
|  |  | ||||||
| ## from peewee_async docs: | ## 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 | # with manager! It’s all automatic. But you can run Manager.connect() or | ||||||
| # Manager.close() when you need it. | # 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: |        database.allow_sync = False | ||||||
| class KeyValue(peewee.Model): |     """ | ||||||
|     key = peewee.CharField(max_length=40, unique=True) |  | ||||||
|     text = peewee.TextField(default='') |     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: |         class Meta: | ||||||
|         database = database |             database=db | ||||||
|  |     return _Base | ||||||
| # create table synchronously |  | ||||||
| KeyValue.create_table(True) |  | ||||||
|  |  | ||||||
| # OPTIONAL: close synchronous connection |  | ||||||
| database.close() |  | ||||||
|  |  | ||||||
| # OPTIONAL: disable any future syncronous calls |  | ||||||
| objects.database.allow_sync = False # this will raise AssertionError on ANY sync call |  | ||||||
|  |  | ||||||
|  |  | ||||||
| app = Sanic('peewee_example') | 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): | AsyncBaseModel = declarative_base(database='test', | ||||||
|     database = PostgresqlDatabase(database='test', |  | ||||||
|                                   host='127.0.0.1', |                                   host='127.0.0.1', | ||||||
|                                   user='postgres', |                                   user='postgres', | ||||||
|                                   password='mysecretpassword') |                                   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>') | @app.route('/post/<key>/<value>') | ||||||
| async def post(request, key, value): | async def post(request, key, value): | ||||||
|     """ |     """ | ||||||
|     Save get parameters to database |     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}) |     return json({'object_id': obj.id}) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -63,7 +100,7 @@ async def get(request): | |||||||
|     """ |     """ | ||||||
|     Load all objects from database |     Load all objects from database | ||||||
|     """ |     """ | ||||||
|     all_objects = await objects.execute(KeyValue.select()) |     all_objects = await KeyValue.objects.execute(KeyValue.select()) | ||||||
|     serialized_obj = [] |     serialized_obj = [] | ||||||
|     for obj in all_objects: |     for obj in all_objects: | ||||||
|         serialized_obj.append({ |         serialized_obj.append({ | ||||||
|   | |||||||
| @@ -1,12 +1,13 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import json | from sanic import response | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
| async def test(request): | async def test(request): | ||||||
|     return json({"test": True}) |     return response.json({"test": True}) | ||||||
|  |  | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000) | if __name__ == '__main__': | ||||||
|  |     app.run(host="0.0.0.0", port=8000) | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import os | |||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.log import log | from sanic.log import log | ||||||
| from sanic.response import json, text, file | from sanic import response | ||||||
| from sanic.exceptions import ServerError | from sanic.exceptions import ServerError | ||||||
|  |  | ||||||
| app = Sanic(__name__) | app = Sanic(__name__) | ||||||
| @@ -10,17 +10,17 @@ app = Sanic(__name__) | |||||||
|  |  | ||||||
| @app.route("/") | @app.route("/") | ||||||
| async def test_async(request): | async def test_async(request): | ||||||
|     return json({"test": True}) |     return response.json({"test": True}) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/sync", methods=['GET', 'POST']) | @app.route("/sync", methods=['GET', 'POST']) | ||||||
| def test_sync(request): | def test_sync(request): | ||||||
|     return json({"test": True}) |     return response.json({"test": True}) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/dynamic/<name>/<id:int>") | @app.route("/dynamic/<name>/<id:int>") | ||||||
| def test_params(request, name, id): | def test_params(request, name, id): | ||||||
|     return text("yeehaww {} {}".format(name, id)) |     return response.text("yeehaww {} {}".format(name, id)) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/exception") | @app.route("/exception") | ||||||
| @@ -31,11 +31,11 @@ def exception(request): | |||||||
| async def test_await(request): | async def test_await(request): | ||||||
|     import asyncio |     import asyncio | ||||||
|     await asyncio.sleep(5) |     await asyncio.sleep(5) | ||||||
|     return text("I'm feeling sleepy") |     return response.text("I'm feeling sleepy") | ||||||
|  |  | ||||||
| @app.route("/file") | @app.route("/file") | ||||||
| async def test_file(request): | 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) | @app.exception(ServerError) | ||||||
| async def test(request, exception): | 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") | @app.route("/json") | ||||||
| def post_json(request): | def post_json(request): | ||||||
|     return json({"received": True, "message": request.json}) |     return response.json({"received": True, "message": request.json}) | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.route("/form") | @app.route("/form") | ||||||
| def post_json(request): | 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") | @app.route("/query_string") | ||||||
| def query_string(request): | 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 | # Run Server | ||||||
| # ----------------------------------------------- # | # ----------------------------------------------- # | ||||||
|  |  | ||||||
|  | @app.listener('before_server_start') | ||||||
|  | def before_start(app, loop): | ||||||
|  |     log.info("SERVER STARTING") | ||||||
|  |  | ||||||
|  |  | ||||||
| @app.listener('after_server_start') | @app.listener('after_server_start') | ||||||
| def after_start(app, loop): | def after_start(app, loop): | ||||||
|     log.info("OH OH OH OH OHHHHHHHH") |     log.info("OH OH OH OH OHHHHHHHH") | ||||||
| @@ -77,7 +82,13 @@ def after_start(app, loop): | |||||||
|  |  | ||||||
| @app.listener('before_server_stop') | @app.listener('before_server_stop') | ||||||
| def before_stop(app, loop): | def before_stop(app, loop): | ||||||
|  |     log.info("SERVER STOPPING") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @app.listener('after_server_stop') | ||||||
|  | def after_stop(app, loop): | ||||||
|     log.info("TRIED EVERYTHING") |     log.info("TRIED EVERYTHING") | ||||||
|  |  | ||||||
|  |  | ||||||
| app.run(host="0.0.0.0", port=8000, debug=True) | 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 import Sanic | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
|  |  | ||||||
| @@ -15,23 +15,23 @@ bp = Blueprint("bp", host="bp.example.com") | |||||||
|                       "somethingelse.com", |                       "somethingelse.com", | ||||||
|                       "therestofyourdomains.com"]) |                       "therestofyourdomains.com"]) | ||||||
| async def hello(request): | async def hello(request): | ||||||
|     return text("Some defaults") |     return response.text("Some defaults") | ||||||
|  |  | ||||||
| @app.route('/', host="example.com") | @app.route('/', host="example.com") | ||||||
| async def hello(request): | async def hello(request): | ||||||
|     return text("Answer") |     return response.text("Answer") | ||||||
|  |  | ||||||
| @app.route('/', host="sub.example.com") | @app.route('/', host="sub.example.com") | ||||||
| async def hello(request): | async def hello(request): | ||||||
|     return text("42") |     return response.text("42") | ||||||
|  |  | ||||||
| @bp.route("/question") | @bp.route("/question") | ||||||
| async def hello(request): | async def hello(request): | ||||||
|     return text("What is the meaning of life?") |     return response.text("What is the meaning of life?") | ||||||
|  |  | ||||||
| @bp.route("/answer") | @bp.route("/answer") | ||||||
| async def hello(request): | async def hello(request): | ||||||
|     return text("42") |     return response.text("42") | ||||||
|  |  | ||||||
| app.register_blueprint(bp) | 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 | aiofiles | ||||||
| aiohttp | aiohttp==1.3.5 | ||||||
|  | chardet<=2.3.0 | ||||||
| beautifulsoup4 | beautifulsoup4 | ||||||
| bottle |  | ||||||
| coverage | coverage | ||||||
| falcon |  | ||||||
| gunicorn |  | ||||||
| httptools | httptools | ||||||
| kyoukai | flake8 | ||||||
| pytest | pytest | ||||||
| recommonmark |  | ||||||
| sphinx |  | ||||||
| sphinx_rtd_theme |  | ||||||
| tornado |  | ||||||
| tox | tox | ||||||
| ujson | ujson | ||||||
| uvloop | uvloop | ||||||
|   | |||||||
| @@ -2,3 +2,4 @@ aiofiles | |||||||
| httptools | httptools | ||||||
| ujson | ujson | ||||||
| uvloop | uvloop | ||||||
|  | websockets | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| from sanic.app import Sanic | from sanic.app import Sanic | ||||||
| from sanic.blueprints import Blueprint | from sanic.blueprints import Blueprint | ||||||
|  |  | ||||||
| __version__ = '0.4.0' | __version__ = '0.5.1' | ||||||
|  |  | ||||||
| __all__ = ['Sanic', 'Blueprint'] | __all__ = ['Sanic', 'Blueprint'] | ||||||
|   | |||||||
| @@ -8,6 +8,10 @@ if __name__ == "__main__": | |||||||
|     parser = ArgumentParser(prog='sanic') |     parser = ArgumentParser(prog='sanic') | ||||||
|     parser.add_argument('--host', dest='host', type=str, default='127.0.0.1') |     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('--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('--workers', dest='workers', type=int, default=1, ) | ||||||
|     parser.add_argument('--debug', dest='debug', action="store_true") |     parser.add_argument('--debug', dest='debug', action="store_true") | ||||||
|     parser.add_argument('module') |     parser.add_argument('module') | ||||||
| @@ -24,9 +28,13 @@ if __name__ == "__main__": | |||||||
|             raise ValueError("Module is not a Sanic app, it is a {}.  " |             raise ValueError("Module is not a Sanic app, it is a {}.  " | ||||||
|                              "Perhaps you meant {}.app?" |                              "Perhaps you meant {}.app?" | ||||||
|                              .format(type(app).__name__, args.module)) |                              .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, |         app.run(host=args.host, port=args.port, | ||||||
|                 workers=args.workers, debug=args.debug) |                 workers=args.workers, debug=args.debug, ssl=ssl) | ||||||
|     except ImportError: |     except ImportError: | ||||||
|         log.error("No module named {} found.\n" |         log.error("No module named {} found.\n" | ||||||
|                   "  Example File: project/sanic_server.py -> app\n" |                   "  Example File: project/sanic_server.py -> app\n" | ||||||
|   | |||||||
							
								
								
									
										255
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										255
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -1,29 +1,32 @@ | |||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| import warnings | import warnings | ||||||
| from asyncio import get_event_loop | from asyncio import get_event_loop, ensure_future, CancelledError | ||||||
| from collections import deque, defaultdict | from collections import deque, defaultdict | ||||||
| from functools import partial | from functools import partial | ||||||
| from inspect import isawaitable, stack, getmodulename | from inspect import isawaitable, stack, getmodulename | ||||||
| from traceback import format_exc | from traceback import format_exc | ||||||
| from urllib.parse import urlencode, urlunparse | from urllib.parse import urlencode, urlunparse | ||||||
|  | from ssl import create_default_context, Purpose | ||||||
|  |  | ||||||
| from sanic.config import Config | from sanic.config import Config | ||||||
| from sanic.constants import HTTP_METHODS | from sanic.constants import HTTP_METHODS | ||||||
| from sanic.exceptions import ServerError, URLBuildError, SanicException | from sanic.exceptions import ServerError, URLBuildError, SanicException | ||||||
| from sanic.handlers import ErrorHandler | from sanic.handlers import ErrorHandler | ||||||
| from sanic.log import log | from sanic.log import log | ||||||
| from sanic.response import HTTPResponse | from sanic.response import HTTPResponse, StreamingHTTPResponse | ||||||
| from sanic.router import Router | from sanic.router import Router | ||||||
| from sanic.server import serve, serve_multiple, HttpProtocol | from sanic.server import serve, serve_multiple, HttpProtocol | ||||||
| from sanic.static import register as static_register | 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.views import CompositionView | ||||||
|  | from sanic.websocket import WebSocketProtocol, ConnectionClosed | ||||||
|  |  | ||||||
|  |  | ||||||
| class Sanic: | 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 |         # Only set up a default log handler if the | ||||||
|         # end-user application didn't set anything up. |         # end-user application didn't set anything up. | ||||||
|         if not logging.root.handlers and log.level == logging.NOTSET: |         if not logging.root.handlers and log.level == logging.NOTSET: | ||||||
| @@ -41,8 +44,9 @@ class Sanic: | |||||||
|  |  | ||||||
|         self.name = name |         self.name = name | ||||||
|         self.router = router or Router() |         self.router = router or Router() | ||||||
|  |         self.request_class = request_class | ||||||
|         self.error_handler = error_handler or ErrorHandler() |         self.error_handler = error_handler or ErrorHandler() | ||||||
|         self.config = Config() |         self.config = Config(load_env=load_env) | ||||||
|         self.request_middleware = deque() |         self.request_middleware = deque() | ||||||
|         self.response_middleware = deque() |         self.response_middleware = deque() | ||||||
|         self.blueprints = {} |         self.blueprints = {} | ||||||
| @@ -51,6 +55,8 @@ class Sanic: | |||||||
|         self.sock = None |         self.sock = None | ||||||
|         self.listeners = defaultdict(list) |         self.listeners = defaultdict(list) | ||||||
|         self.is_running = False |         self.is_running = False | ||||||
|  |         self.websocket_enabled = False | ||||||
|  |         self.websocket_tasks = [] | ||||||
|  |  | ||||||
|         # Register alternative method names |         # Register alternative method names | ||||||
|         self.go_fast = self.run |         self.go_fast = self.run | ||||||
| @@ -98,7 +104,8 @@ class Sanic: | |||||||
|         return decorator |         return decorator | ||||||
|  |  | ||||||
|     # 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 |         """Decorate a function to be registered as a route | ||||||
|  |  | ||||||
|         :param uri: path of the URL |         :param uri: path of the URL | ||||||
| @@ -114,34 +121,42 @@ class Sanic: | |||||||
|  |  | ||||||
|         def response(handler): |         def response(handler): | ||||||
|             self.router.add(uri=uri, methods=methods, handler=handler, |             self.router.add(uri=uri, methods=methods, handler=handler, | ||||||
|                             host=host) |                             host=host, strict_slashes=strict_slashes) | ||||||
|             return handler |             return handler | ||||||
|  |  | ||||||
|         return response |         return response | ||||||
|  |  | ||||||
|     # Shorthand method decorators |     # Shorthand method decorators | ||||||
|     def get(self, uri, host=None): |     def get(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=frozenset({"GET"}), host=host) |         return self.route(uri, methods=frozenset({"GET"}), host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def post(self, uri, host=None): |     def post(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=frozenset({"POST"}), host=host) |         return self.route(uri, methods=frozenset({"POST"}), host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def put(self, uri, host=None): |     def put(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=frozenset({"PUT"}), host=host) |         return self.route(uri, methods=frozenset({"PUT"}), host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def head(self, uri, host=None): |     def head(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=frozenset({"HEAD"}), host=host) |         return self.route(uri, methods=frozenset({"HEAD"}), host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def options(self, uri, host=None): |     def options(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=frozenset({"OPTIONS"}), host=host) |         return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def patch(self, uri, host=None): |     def patch(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=frozenset({"PATCH"}), host=host) |         return self.route(uri, methods=frozenset({"PATCH"}), host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def delete(self, uri, host=None): |     def delete(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=frozenset({"DELETE"}), host=host) |         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 |         """A helper method to register class instance or | ||||||
|         functions as a handler to the application url |         functions as a handler to the application url | ||||||
|         routes. |         routes. | ||||||
| @@ -165,9 +180,71 @@ class Sanic: | |||||||
|         if isinstance(handler, CompositionView): |         if isinstance(handler, CompositionView): | ||||||
|             methods = handler.handlers.keys() |             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 |         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): |     def remove_route(self, uri, clean_cache=True, host=None): | ||||||
|         self.router.remove(uri, clean_cache, host) |         self.router.remove(uri, clean_cache, host) | ||||||
|  |  | ||||||
| @@ -181,6 +258,10 @@ class Sanic: | |||||||
|  |  | ||||||
|         def response(handler): |         def response(handler): | ||||||
|             for exception in exceptions: |             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) |                     self.error_handler.add(exception, handler) | ||||||
|             return handler |             return handler | ||||||
|  |  | ||||||
| @@ -254,7 +335,7 @@ class Sanic: | |||||||
|         the output URL's query string. |         the output URL's query string. | ||||||
|  |  | ||||||
|         :param view_name: string referencing the view name |         :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. |             parameters and query string arguments. | ||||||
|  |  | ||||||
|         :return: the built URL |         :return: the built URL | ||||||
| @@ -345,14 +426,17 @@ class Sanic: | |||||||
|     def converted_response_type(self, response): |     def converted_response_type(self, response): | ||||||
|         pass |         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 |         """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 |         to be sent back The HTTP Server only expects a response object, so | ||||||
|         exception handling must be done here |         exception handling must be done here | ||||||
|  |  | ||||||
|         :param request: HTTP Request object |         :param request: HTTP Request object | ||||||
|         :param response_callback: Response function to be called with the |         :param write_callback: Synchronous response function to be | ||||||
|                                   response as the only argument |             called with the response as the only argument | ||||||
|  |         :param stream_callback: Coroutine that handles streaming a | ||||||
|  |             StreamingHTTPResponse if produced by the handler. | ||||||
|  |  | ||||||
|         :return: Nothing |         :return: Nothing | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
| @@ -361,17 +445,7 @@ class Sanic: | |||||||
|             # -------------------------------------------- # |             # -------------------------------------------- # | ||||||
|  |  | ||||||
|             request.app = self |             request.app = self | ||||||
|  |             response = await self._run_request_middleware(request) | ||||||
|             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 |  | ||||||
|  |  | ||||||
|             # No middleware results |             # No middleware results | ||||||
|             if not response: |             if not response: | ||||||
|                 # -------------------------------------------- # |                 # -------------------------------------------- # | ||||||
| @@ -389,20 +463,6 @@ class Sanic: | |||||||
|                 response = handler(request, *args, **kwargs) |                 response = handler(request, *args, **kwargs) | ||||||
|                 if isawaitable(response): |                 if isawaitable(response): | ||||||
|                     response = await 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: |         except Exception as e: | ||||||
|             # -------------------------------------------- # |             # -------------------------------------------- # | ||||||
|             # Response Generation Failed |             # Response Generation Failed | ||||||
| @@ -420,8 +480,23 @@ class Sanic: | |||||||
|                 else: |                 else: | ||||||
|                     response = HTTPResponse( |                     response = HTTPResponse( | ||||||
|                         "An error occurred while handling an error") |                         "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 |     # Testing | ||||||
| @@ -429,7 +504,7 @@ class Sanic: | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def test_client(self): |     def test_client(self): | ||||||
|         return TestClient(self) |         return SanicTestClient(self) | ||||||
|  |  | ||||||
|     # -------------------------------------------------------------------- # |     # -------------------------------------------------------------------- # | ||||||
|     # Execution |     # Execution | ||||||
| @@ -437,7 +512,7 @@ class Sanic: | |||||||
|  |  | ||||||
|     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, |     def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, | ||||||
|             after_start=None, before_stop=None, after_stop=None, ssl=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): |             backlog=100, stop_event=None, register_sys_signals=True): | ||||||
|         """Run the HTTP Server and listen until keyboard interrupt or term |         """Run the HTTP Server and listen until keyboard interrupt or term | ||||||
|         signal. On termination, drain connections before closing. |         signal. On termination, drain connections before closing. | ||||||
| @@ -453,7 +528,8 @@ class Sanic: | |||||||
|                             received before it is respected |                             received before it is respected | ||||||
|         :param after_stop: Functions to be executed when all requests are |         :param after_stop: Functions to be executed when all requests are | ||||||
|                             complete |                             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 sock: Socket for the server to accept connections from | ||||||
|         :param workers: Number of processes |         :param workers: Number of processes | ||||||
|                             received before it is respected |                             received before it is respected | ||||||
| @@ -464,19 +540,27 @@ class Sanic: | |||||||
|         :param protocol: Subclass of asyncio protocol class |         :param protocol: Subclass of asyncio protocol class | ||||||
|         :return: Nothing |         :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( |         server_settings = self._helper( | ||||||
|             host=host, port=port, debug=debug, before_start=before_start, |             host=host, port=port, debug=debug, before_start=before_start, | ||||||
|             after_start=after_start, before_stop=before_stop, |             after_start=after_start, before_stop=before_stop, | ||||||
|             after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, |             after_stop=after_stop, ssl=ssl, sock=sock, workers=workers, | ||||||
|             loop=loop, protocol=protocol, backlog=backlog, |             loop=loop, protocol=protocol, backlog=backlog, | ||||||
|             stop_event=stop_event, register_sys_signals=register_sys_signals) |             register_sys_signals=register_sys_signals) | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             self.is_running = True |             self.is_running = True | ||||||
|             if workers == 1: |             if workers == 1: | ||||||
|                 serve(**server_settings) |                 serve(**server_settings) | ||||||
|             else: |             else: | ||||||
|                 serve_multiple(server_settings, workers, stop_event) |                 serve_multiple(server_settings, workers) | ||||||
|         except: |         except: | ||||||
|             log.exception( |             log.exception( | ||||||
|                 'Experienced exception while trying to serve') |                 'Experienced exception while trying to serve') | ||||||
| @@ -488,26 +572,59 @@ class Sanic: | |||||||
|         """This kills the Sanic""" |         """This kills the Sanic""" | ||||||
|         get_event_loop().stop() |         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, |     async def create_server(self, host="127.0.0.1", port=8000, debug=False, | ||||||
|                             before_start=None, after_start=None, |                             before_start=None, after_start=None, | ||||||
|                             before_stop=None, after_stop=None, ssl=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): |                             backlog=100, stop_event=None): | ||||||
|         """Asynchronous version of `run`. |         """Asynchronous version of `run`. | ||||||
|  |  | ||||||
|         NOTE: This does not support multiprocessing and is not the preferred |         NOTE: This does not support multiprocessing and is not the preferred | ||||||
|               way to run a Sanic application. |               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( |         server_settings = self._helper( | ||||||
|             host=host, port=port, debug=debug, before_start=before_start, |             host=host, port=port, debug=debug, before_start=before_start, | ||||||
|             after_start=after_start, before_stop=before_stop, |             after_start=after_start, before_stop=before_stop, | ||||||
|             after_stop=after_stop, ssl=ssl, sock=sock, |             after_stop=after_stop, ssl=ssl, sock=sock, | ||||||
|             loop=loop or get_event_loop(), protocol=protocol, |             loop=loop or get_event_loop(), protocol=protocol, | ||||||
|             backlog=backlog, stop_event=stop_event, |             backlog=backlog, run_async=True) | ||||||
|             run_async=True) |  | ||||||
|  |  | ||||||
|         return await serve(**server_settings) |         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, |     def _helper(self, host="127.0.0.1", port=8000, debug=False, | ||||||
|                 before_start=None, after_start=None, before_stop=None, |                 before_start=None, after_start=None, before_stop=None, | ||||||
|                 after_stop=None, ssl=None, sock=None, workers=1, loop=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): |                 register_sys_signals=True, run_async=False): | ||||||
|         """Helper function used by `run` and `create_server`.""" |         """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 loop is not None: | ||||||
|             if debug: |             if debug: | ||||||
|                 warnings.simplefilter('default') |                 warnings.simplefilter('default') | ||||||
| @@ -538,6 +669,7 @@ class Sanic: | |||||||
|  |  | ||||||
|         server_settings = { |         server_settings = { | ||||||
|             'protocol': protocol, |             'protocol': protocol, | ||||||
|  |             'request_class': self.request_class, | ||||||
|             'host': host, |             'host': host, | ||||||
|             'port': port, |             'port': port, | ||||||
|             'sock': sock, |             'sock': sock, | ||||||
| @@ -583,6 +715,7 @@ class Sanic: | |||||||
|             server_settings['run_async'] = True |             server_settings['run_async'] = True | ||||||
|  |  | ||||||
|         # Serve |         # Serve | ||||||
|  |         if host and port: | ||||||
|             proto = "http" |             proto = "http" | ||||||
|             if ssl is not None: |             if ssl is not None: | ||||||
|                 proto = "https" |                 proto = "https" | ||||||
|   | |||||||
| @@ -3,7 +3,9 @@ from collections import defaultdict, namedtuple | |||||||
| from sanic.constants import HTTP_METHODS | from sanic.constants import HTTP_METHODS | ||||||
| from sanic.views import CompositionView | 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']) | FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) | ||||||
| FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) | FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) | ||||||
| FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) | FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) | ||||||
| @@ -23,6 +25,7 @@ class Blueprint: | |||||||
|         self.host = host |         self.host = host | ||||||
|  |  | ||||||
|         self.routes = [] |         self.routes = [] | ||||||
|  |         self.websocket_routes = [] | ||||||
|         self.exceptions = [] |         self.exceptions = [] | ||||||
|         self.listeners = defaultdict(list) |         self.listeners = defaultdict(list) | ||||||
|         self.middlewares = [] |         self.middlewares = [] | ||||||
| @@ -43,7 +46,20 @@ class Blueprint: | |||||||
|             app.route( |             app.route( | ||||||
|                 uri=uri[1:] if uri.startswith('//') else uri, |                 uri=uri[1:] if uri.startswith('//') else uri, | ||||||
|                 methods=future.methods, |                 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) |                 )(future.handler) | ||||||
|  |  | ||||||
|         # Middleware |         # Middleware | ||||||
| @@ -70,19 +86,21 @@ class Blueprint: | |||||||
|             for listener in listeners: |             for listener in listeners: | ||||||
|                 app.listener(event)(listener) |                 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. |         """Create a blueprint route from a decorated function. | ||||||
|  |  | ||||||
|         :param uri: endpoint at which the route will be accessible. |         :param uri: endpoint at which the route will be accessible. | ||||||
|         :param methods: list of acceptable HTTP methods. |         :param methods: list of acceptable HTTP methods. | ||||||
|         """ |         """ | ||||||
|         def decorator(handler): |         def decorator(handler): | ||||||
|             route = FutureRoute(handler, uri, methods, host) |             route = FutureRoute(handler, uri, methods, host, strict_slashes) | ||||||
|             self.routes.append(route) |             self.routes.append(route) | ||||||
|             return handler |             return handler | ||||||
|         return decorator |         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. |         """Create a blueprint route from a function. | ||||||
|  |  | ||||||
|         :param handler: function for handling uri requests. Accepts function, |         :param handler: function for handling uri requests. Accepts function, | ||||||
| @@ -103,7 +121,30 @@ class Blueprint: | |||||||
|         if isinstance(handler, CompositionView): |         if isinstance(handler, CompositionView): | ||||||
|             methods = handler.handlers.keys() |             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 |         return handler | ||||||
|  |  | ||||||
|     def listener(self, event): |     def listener(self, event): | ||||||
| @@ -149,23 +190,30 @@ class Blueprint: | |||||||
|         self.statics.append(static) |         self.statics.append(static) | ||||||
|  |  | ||||||
|     # Shorthand method decorators |     # Shorthand method decorators | ||||||
|     def get(self, uri, host=None): |     def get(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=["GET"], host=host) |         return self.route(uri, methods=["GET"], host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def post(self, uri, host=None): |     def post(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=["POST"], host=host) |         return self.route(uri, methods=["POST"], host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def put(self, uri, host=None): |     def put(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=["PUT"], host=host) |         return self.route(uri, methods=["PUT"], host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def head(self, uri, host=None): |     def head(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=["HEAD"], host=host) |         return self.route(uri, methods=["HEAD"], host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def options(self, uri, host=None): |     def options(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=["OPTIONS"], host=host) |         return self.route(uri, methods=["OPTIONS"], host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def patch(self, uri, host=None): |     def patch(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=["PATCH"], host=host) |         return self.route(uri, methods=["PATCH"], host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|  |  | ||||||
|     def delete(self, uri, host=None): |     def delete(self, uri, host=None, strict_slashes=False): | ||||||
|         return self.route(uri, methods=["DELETE"], host=host) |         return self.route(uri, methods=["DELETE"], host=host, | ||||||
|  |                           strict_slashes=strict_slashes) | ||||||
|   | |||||||
| @@ -1,9 +1,12 @@ | |||||||
| import os | import os | ||||||
|  |  | ||||||
| import types | import types | ||||||
|  |  | ||||||
|  | SANIC_PREFIX = 'SANIC_' | ||||||
|  |  | ||||||
|  |  | ||||||
| class Config(dict): | class Config(dict): | ||||||
|     def __init__(self, defaults=None): |     def __init__(self, defaults=None, load_env=True): | ||||||
|         super().__init__(defaults or {}) |         super().__init__(defaults or {}) | ||||||
|         self.LOGO = """ |         self.LOGO = """ | ||||||
|                  ▄▄▄▄▄ |                  ▄▄▄▄▄ | ||||||
| @@ -29,6 +32,9 @@ class Config(dict): | |||||||
|         self.REQUEST_MAX_SIZE = 100000000  # 100 megababies |         self.REQUEST_MAX_SIZE = 100000000  # 100 megababies | ||||||
|         self.REQUEST_TIMEOUT = 60  # 60 seconds |         self.REQUEST_TIMEOUT = 60  # 60 seconds | ||||||
|  |  | ||||||
|  |         if load_env: | ||||||
|  |             self.load_environment_vars() | ||||||
|  |  | ||||||
|     def __getattr__(self, attr): |     def __getattr__(self, attr): | ||||||
|         try: |         try: | ||||||
|             return self[attr] |             return self[attr] | ||||||
| @@ -90,3 +96,13 @@ class Config(dict): | |||||||
|         for key in dir(obj): |         for key in dir(obj): | ||||||
|             if key.isupper(): |             if key.isupper(): | ||||||
|                 self[key] = getattr(obj, key) |                 self[key] = getattr(obj, key) | ||||||
|  |  | ||||||
|  |     def load_environment_vars(self): | ||||||
|  |         for k, v in os.environ.items(): | ||||||
|  |             """ | ||||||
|  |             Looks for any SANIC_ prefixed environment variables and applies | ||||||
|  |             them to the configuration if present. | ||||||
|  |             """ | ||||||
|  |             if k.startswith(SANIC_PREFIX): | ||||||
|  |                 _, config_key = k.split(SANIC_PREFIX, 1) | ||||||
|  |                 self[config_key] = v | ||||||
|   | |||||||
| @@ -19,7 +19,7 @@ _Translator.update({ | |||||||
|  |  | ||||||
|  |  | ||||||
| def _quote(str): | 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 |     If the string does not need to be double-quoted, then just return the | ||||||
|     string.  Otherwise, surround the string in doublequotes and quote |     string.  Otherwise, surround the string in doublequotes and quote | ||||||
|     (with a \) special characters. |     (with a \) special characters. | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ TRACEBACK_WRAPPER_HTML = ''' | |||||||
|                 {frame_html} |                 {frame_html} | ||||||
|                 <p class="summary"> |                 <p class="summary"> | ||||||
|                 <b>{exc_name}: {exc_value}</b> |                 <b>{exc_name}: {exc_value}</b> | ||||||
|                     while handling uri <code>{uri}</code> |                     while handling path <code>{path}</code> | ||||||
|                 </p> |                 </p> | ||||||
|             </div> |             </div> | ||||||
|         </body> |         </body> | ||||||
|   | |||||||
| @@ -16,9 +16,12 @@ from sanic.response import text, html | |||||||
|  |  | ||||||
| class ErrorHandler: | class ErrorHandler: | ||||||
|     handlers = None |     handlers = None | ||||||
|  |     cached_handlers = None | ||||||
|  |     _missing = object() | ||||||
|  |  | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         self.handlers = {} |         self.handlers = [] | ||||||
|  |         self.cached_handlers = {} | ||||||
|         self.debug = False |         self.debug = False | ||||||
|  |  | ||||||
|     def _render_traceback_html(self, exception, request): |     def _render_traceback_html(self, exception, request): | ||||||
| @@ -34,10 +37,21 @@ class ErrorHandler: | |||||||
|             exc_name=exc_type.__name__, |             exc_name=exc_type.__name__, | ||||||
|             exc_value=exc_value, |             exc_value=exc_value, | ||||||
|             frame_html=''.join(frame_html), |             frame_html=''.join(frame_html), | ||||||
|             uri=request.url) |             path=request.path) | ||||||
|  |  | ||||||
|     def add(self, exception, handler): |     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): |     def response(self, request, exception): | ||||||
|         """Fetches and executes an exception handler and returns a response |         """Fetches and executes an exception handler and returns a response | ||||||
| @@ -47,9 +61,13 @@ class ErrorHandler: | |||||||
|         :param exception: Exception to handle |         :param exception: Exception to handle | ||||||
|         :return: Response object |         :return: Response object | ||||||
|         """ |         """ | ||||||
|         handler = self.handlers.get(type(exception), self.default) |         handler = self.lookup(exception) | ||||||
|  |         response = None | ||||||
|         try: |         try: | ||||||
|  |             if handler: | ||||||
|                 response = handler(request=request, exception=exception) |                 response = handler(request=request, exception=exception) | ||||||
|  |             if response is None: | ||||||
|  |                 response = self.default(request=request, exception=exception) | ||||||
|         except Exception: |         except Exception: | ||||||
|             self.log(format_exc()) |             self.log(format_exc()) | ||||||
|             if self.debug: |             if self.debug: | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ from cgi import parse_header | |||||||
| from collections import namedtuple | from collections import namedtuple | ||||||
| from http.cookies import SimpleCookie | from http.cookies import SimpleCookie | ||||||
| from httptools import parse_url | from httptools import parse_url | ||||||
| from urllib.parse import parse_qs | from urllib.parse import parse_qs, urlunparse | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     from ujson import loads as json_loads |     from ujson import loads as json_loads | ||||||
| @@ -36,24 +36,20 @@ class RequestParameters(dict): | |||||||
| class Request(dict): | class Request(dict): | ||||||
|     """Properties of an HTTP request such as URL, headers, etc.""" |     """Properties of an HTTP request such as URL, headers, etc.""" | ||||||
|     __slots__ = ( |     __slots__ = ( | ||||||
|         'app', 'url', 'headers', 'version', 'method', '_cookies', 'transport', |         'app', 'headers', 'version', 'method', '_cookies', 'transport', | ||||||
|         'query_string', 'body', |         'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', | ||||||
|         'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', |         '_ip', '_parsed_url', | ||||||
|         '_ip', |  | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     def __init__(self, url_bytes, headers, version, method, transport): |     def __init__(self, url_bytes, headers, version, method, transport): | ||||||
|         # TODO: Content-Encoding detection |         # TODO: Content-Encoding detection | ||||||
|         url_parsed = parse_url(url_bytes) |         self._parsed_url = parse_url(url_bytes) | ||||||
|         self.app = None |         self.app = None | ||||||
|         self.url = url_parsed.path.decode('utf-8') |  | ||||||
|         self.headers = headers |         self.headers = headers | ||||||
|         self.version = version |         self.version = version | ||||||
|         self.method = method |         self.method = method | ||||||
|         self.transport = transport |         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 |         # Init but do not inhale | ||||||
|         self.body = [] |         self.body = [] | ||||||
| @@ -69,6 +65,8 @@ class Request(dict): | |||||||
|             try: |             try: | ||||||
|                 self.parsed_json = json_loads(self.body) |                 self.parsed_json = json_loads(self.body) | ||||||
|             except Exception: |             except Exception: | ||||||
|  |                 if not self.body: | ||||||
|  |                     return None | ||||||
|                 raise InvalidUsage("Failed when parsing body as json") |                 raise InvalidUsage("Failed when parsing body as json") | ||||||
|  |  | ||||||
|         return self.parsed_json |         return self.parsed_json | ||||||
| @@ -123,6 +121,10 @@ class Request(dict): | |||||||
|                 self.parsed_args = RequestParameters() |                 self.parsed_args = RequestParameters() | ||||||
|         return self.parsed_args |         return self.parsed_args | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def raw_args(self): | ||||||
|  |         return {k: v[0] for k, v in self.args.items()} | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def cookies(self): |     def cookies(self): | ||||||
|         if self._cookies is None: |         if self._cookies is None: | ||||||
| @@ -142,6 +144,46 @@ class Request(dict): | |||||||
|             self._ip = self.transport.get_extra_info('peername') |             self._ip = self.transport.get_extra_info('peername') | ||||||
|         return self._ip |         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']) | File = namedtuple('File', ['type', 'body', 'name']) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,10 @@ | |||||||
| from mimetypes import guess_type | from mimetypes import guess_type | ||||||
| from os import path | from os import path | ||||||
| from ujson import dumps as json_dumps |  | ||||||
|  | try: | ||||||
|  |     from ujson import dumps as json_dumps | ||||||
|  | except: | ||||||
|  |     from json import dumps as json_dumps | ||||||
|  |  | ||||||
| from aiofiles import open as open_async | from aiofiles import open as open_async | ||||||
|  |  | ||||||
| @@ -73,37 +77,16 @@ ALL_STATUS_CODES = { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| class HTTPResponse: | class BaseHTTPResponse: | ||||||
|     __slots__ = ('body', 'status', 'content_type', 'headers', '_cookies') |     def _encode_body(self, data): | ||||||
|  |  | ||||||
|     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: |  | ||||||
|         try: |         try: | ||||||
|             # Try to encode it regularly |             # Try to encode it regularly | ||||||
|                 self.body = body.encode() |             return data.encode() | ||||||
|         except AttributeError: |         except AttributeError: | ||||||
|             # Convert it to a str if you can't |             # Convert it to a str if you can't | ||||||
|                 self.body = str(body).encode() |             return str(data).encode() | ||||||
|         else: |  | ||||||
|             self.body = body_bytes |  | ||||||
|  |  | ||||||
|         self.status = status |     def _parse_headers(self): | ||||||
|         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 = b'' |         headers = b'' | ||||||
|         for name, value in self.headers.items(): |         for name, value in self.headers.items(): | ||||||
|             try: |             try: | ||||||
| @@ -115,6 +98,114 @@ class HTTPResponse: | |||||||
|                     b'%b: %b\r\n' % ( |                     b'%b: %b\r\n' % ( | ||||||
|                         str(name).encode(), str(value).encode('utf-8'))) |                         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 |         # Try to pull from the common codes first | ||||||
|         # Speeds up response rate 6% over pulling from all |         # Speeds up response rate 6% over pulling from all | ||||||
|         status = COMMON_STATUS_CODES.get(self.status) |         status = COMMON_STATUS_CODES.get(self.status) | ||||||
| @@ -161,10 +252,10 @@ def text(body, status=200, headers=None, | |||||||
|     :param body: Response data to be encoded. |     :param body: Response data to be encoded. | ||||||
|     :param status: Response code. |     :param status: Response code. | ||||||
|     :param headers: Custom Headers. |     :param headers: Custom Headers. | ||||||
|     :param content_type: |     :param content_type: the content type (string) of the response | ||||||
|         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) |         content_type=content_type) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -175,8 +266,7 @@ def raw(body, status=200, headers=None, | |||||||
|     :param body: Response data. |     :param body: Response data. | ||||||
|     :param status: Response code. |     :param status: Response code. | ||||||
|     :param headers: Custom Headers. |     :param headers: Custom Headers. | ||||||
|     :param content_type: |     :param content_type: the content type (string) of the response. | ||||||
|         the content type (string) of the response |  | ||||||
|     """ |     """ | ||||||
|     return HTTPResponse(body_bytes=body, status=status, headers=headers, |     return HTTPResponse(body_bytes=body, status=status, headers=headers, | ||||||
|                         content_type=content_type) |                         content_type=content_type) | ||||||
| @@ -220,6 +310,35 @@ async def file(location, mime_type=None, headers=None, _range=None): | |||||||
|                         body_bytes=out_stream) |                         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, | def redirect(to, headers=None, status=302, | ||||||
|              content_type="text/html; charset=utf-8"): |              content_type="text/html; charset=utf-8"): | ||||||
|     """Abort execution and cause a 302 redirect (by default). |     """Abort execution and cause a 302 redirect (by default). | ||||||
|   | |||||||
| @@ -75,8 +75,9 @@ class Router: | |||||||
|         """Parse a parameter string into its constituent name, type, and |         """Parse a parameter string into its constituent name, type, and | ||||||
|         pattern |         pattern | ||||||
|  |  | ||||||
|         For example: |         For example:: | ||||||
|         `parse_parameter_string('<param_one:[A-z]>')` -> |  | ||||||
|  |             parse_parameter_string('<param_one:[A-z]>')` -> | ||||||
|                 ('param_one', str, '[A-z]') |                 ('param_one', str, '[A-z]') | ||||||
|  |  | ||||||
|         :param parameter_string: String to parse |         :param parameter_string: String to parse | ||||||
| @@ -95,13 +96,24 @@ class Router: | |||||||
|  |  | ||||||
|         return name, _type, pattern |         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 |         # add regular version | ||||||
|         self._add(uri, methods, handler, host) |         self._add(uri, methods, handler, host) | ||||||
|         slash_is_missing = (not uri[-1].endswith('/') |  | ||||||
|                             and not self.routes_all.get(uri + '/', False)) |         if strict_slashes: | ||||||
|         without_slash_is_missing = (not self.routes_all.get(uri[:-1], False) |             return | ||||||
|                                     and uri is not '/') |  | ||||||
|  |         # Add versions with and without trailing / | ||||||
|  |         slash_is_missing = ( | ||||||
|  |             not uri[-1] == '/' | ||||||
|  |             and not self.routes_all.get(uri + '/', False) | ||||||
|  |         ) | ||||||
|  |         without_slash_is_missing = ( | ||||||
|  |             uri[-1] == '/' | ||||||
|  |             and not self.routes_all.get(uri[:-1], False) | ||||||
|  |             and not uri == '/' | ||||||
|  |         ) | ||||||
|         # add version with trailing slash |         # add version with trailing slash | ||||||
|         if slash_is_missing: |         if slash_is_missing: | ||||||
|             self._add(uri + '/', methods, handler, host) |             self._add(uri + '/', methods, handler, host) | ||||||
| @@ -132,9 +144,6 @@ class Router: | |||||||
|                 for host_ in host: |                 for host_ in host: | ||||||
|                     self.add(uri, methods, handler, host_) |                     self.add(uri, methods, handler, host_) | ||||||
|                 return |                 return | ||||||
|         else: |  | ||||||
|             # default host |  | ||||||
|             self.hosts.add('*') |  | ||||||
|  |  | ||||||
|         # Dict for faster lookups of if method allowed |         # Dict for faster lookups of if method allowed | ||||||
|         if methods: |         if methods: | ||||||
| @@ -276,14 +285,14 @@ class Router: | |||||||
|         """ |         """ | ||||||
|         # No virtual hosts specified; default behavior |         # No virtual hosts specified; default behavior | ||||||
|         if not self.hosts: |         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 |         # virtual hosts specified; try to match route to the host header | ||||||
|         try: |         try: | ||||||
|             return self._get(request.url, request.method, |             return self._get(request.path, request.method, | ||||||
|                              request.headers.get("Host", '')) |                              request.headers.get("Host", '')) | ||||||
|         # try default hosts |         # try default hosts | ||||||
|         except NotFound: |         except NotFound: | ||||||
|             return self._get(request.url, request.method, '') |             return self._get(request.path, request.method, '') | ||||||
|  |  | ||||||
|     @lru_cache(maxsize=ROUTER_CACHE_SIZE) |     @lru_cache(maxsize=ROUTER_CACHE_SIZE) | ||||||
|     def _get(self, url, method, host): |     def _get(self, url, method, host): | ||||||
|   | |||||||
							
								
								
									
										103
									
								
								sanic/server.py
									
									
									
									
									
								
							
							
						
						
									
										103
									
								
								sanic/server.py
									
									
									
									
									
								
							| @@ -4,11 +4,17 @@ import traceback | |||||||
| import warnings | import warnings | ||||||
| from functools import partial | from functools import partial | ||||||
| from inspect import isawaitable | from inspect import isawaitable | ||||||
| from multiprocessing import Process, Event | from multiprocessing import Process | ||||||
| from os import set_inheritable | from signal import ( | ||||||
| from signal import SIGTERM, SIGINT |     SIGTERM, SIGINT, | ||||||
| from signal import signal as signal_func |     signal as signal_func, | ||||||
| from socket import socket, SOL_SOCKET, SO_REUSEADDR |     Signals | ||||||
|  | ) | ||||||
|  | from socket import ( | ||||||
|  |     socket, | ||||||
|  |     SOL_SOCKET, | ||||||
|  |     SO_REUSEADDR, | ||||||
|  | ) | ||||||
| from time import time | from time import time | ||||||
|  |  | ||||||
| from httptools import HttpRequestParser | from httptools import HttpRequestParser | ||||||
| @@ -58,12 +64,13 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         'parser', 'request', 'url', 'headers', |         'parser', 'request', 'url', 'headers', | ||||||
|         # request config |         # request config | ||||||
|         'request_handler', 'request_timeout', 'request_max_size', |         'request_handler', 'request_timeout', 'request_max_size', | ||||||
|  |         'request_class', | ||||||
|         # connection management |         # connection management | ||||||
|         '_total_request_size', '_timeout_handler', '_last_communication_time') |         '_total_request_size', '_timeout_handler', '_last_communication_time') | ||||||
|  |  | ||||||
|     def __init__(self, *, loop, request_handler, error_handler, |     def __init__(self, *, loop, request_handler, error_handler, | ||||||
|                  signal=Signal(), connections=set(), request_timeout=60, |                  signal=Signal(), connections=set(), request_timeout=60, | ||||||
|                  request_max_size=None): |                  request_max_size=None, request_class=None): | ||||||
|         self.loop = loop |         self.loop = loop | ||||||
|         self.transport = None |         self.transport = None | ||||||
|         self.request = None |         self.request = None | ||||||
| @@ -76,11 +83,16 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.error_handler = error_handler |         self.error_handler = error_handler | ||||||
|         self.request_timeout = request_timeout |         self.request_timeout = request_timeout | ||||||
|         self.request_max_size = request_max_size |         self.request_max_size = request_max_size | ||||||
|  |         self.request_class = request_class or Request | ||||||
|         self._total_request_size = 0 |         self._total_request_size = 0 | ||||||
|         self._timeout_handler = None |         self._timeout_handler = None | ||||||
|         self._last_request_time = None |         self._last_request_time = None | ||||||
|         self._request_handler_task = None |         self._request_handler_task = None | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def keep_alive(self): | ||||||
|  |         return self.parser.should_keep_alive() and not self.signal.stopped | ||||||
|  |  | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|     # Connection |     # Connection | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
| @@ -145,7 +157,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|         self.headers.append((name.decode().casefold(), value.decode())) |         self.headers.append((name.decode().casefold(), value.decode())) | ||||||
|  |  | ||||||
|     def on_headers_complete(self): |     def on_headers_complete(self): | ||||||
|         self.request = Request( |         self.request = self.request_class( | ||||||
|             url_bytes=self.url, |             url_bytes=self.url, | ||||||
|             headers=CIDict(self.headers), |             headers=CIDict(self.headers), | ||||||
|             version=self.parser.get_http_version(), |             version=self.parser.get_http_version(), | ||||||
| @@ -159,20 +171,59 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|     def on_message_complete(self): |     def on_message_complete(self): | ||||||
|         if self.request.body: |         if self.request.body: | ||||||
|             self.request.body = b''.join(self.request.body) |             self.request.body = b''.join(self.request.body) | ||||||
|  |  | ||||||
|         self._request_handler_task = self.loop.create_task( |         self._request_handler_task = self.loop.create_task( | ||||||
|             self.request_handler(self.request, self.write_response)) |             self.request_handler( | ||||||
|  |                 self.request, | ||||||
|  |                 self.write_response, | ||||||
|  |                 self.stream_response)) | ||||||
|  |  | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|     # Responding |     # Responding | ||||||
|     # -------------------------------------------- # |     # -------------------------------------------- # | ||||||
|  |  | ||||||
|     def write_response(self, response): |     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: |         try: | ||||||
|  |             keep_alive = self.keep_alive | ||||||
|             self.transport.write( |             self.transport.write( | ||||||
|                 response.output( |                 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: |         except AttributeError: | ||||||
|             log.error( |             log.error( | ||||||
|                 ('Invalid response object for url {}, ' |                 ('Invalid response object for url {}, ' | ||||||
| @@ -191,7 +242,6 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             if not keep_alive: |             if not keep_alive: | ||||||
|                 self.transport.close() |                 self.transport.close() | ||||||
|             else: |             else: | ||||||
|                 # Record that we received data |  | ||||||
|                 self._last_request_time = current_time |                 self._last_request_time = current_time | ||||||
|                 self.cleanup() |                 self.cleanup() | ||||||
|  |  | ||||||
| @@ -212,7 +262,7 @@ class HttpProtocol(asyncio.Protocol): | |||||||
|             self.transport.close() |             self.transport.close() | ||||||
|  |  | ||||||
|     def bail_out(self, message, from_error=False): |     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( |             log.error( | ||||||
|                 ("Transport closed @ {} and exception " |                 ("Transport closed @ {} and exception " | ||||||
|                  "experienced during error handling").format( |                  "experienced during error handling").format( | ||||||
| @@ -271,7 +321,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, | |||||||
|           after_start=None, before_stop=None, after_stop=None, debug=False, |           after_start=None, before_stop=None, after_stop=None, debug=False, | ||||||
|           request_timeout=60, ssl=None, sock=None, request_max_size=None, |           request_timeout=60, ssl=None, sock=None, request_max_size=None, | ||||||
|           reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, |           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): | ||||||
|     """Start asynchronous HTTP Server on an individual process. |     """Start asynchronous HTTP Server on an individual process. | ||||||
|  |  | ||||||
|     :param host: Address to host on |     :param host: Address to host on | ||||||
| @@ -296,6 +347,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | |||||||
|     :param reuse_port: `True` for multiple workers |     :param reuse_port: `True` for multiple workers | ||||||
|     :param loop: asyncio compatible event loop |     :param loop: asyncio compatible event loop | ||||||
|     :param protocol: subclass of asyncio protocol class |     :param protocol: subclass of asyncio protocol class | ||||||
|  |     :param request_class: Request class to use | ||||||
|     :return: Nothing |     :return: Nothing | ||||||
|     """ |     """ | ||||||
|     if not run_async: |     if not run_async: | ||||||
| @@ -307,8 +359,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | |||||||
|  |  | ||||||
|     trigger_events(before_start, loop) |     trigger_events(before_start, loop) | ||||||
|  |  | ||||||
|     connections = set() |     connections = connections if connections is not None else set() | ||||||
|     signal = Signal() |  | ||||||
|     server = partial( |     server = partial( | ||||||
|         protocol, |         protocol, | ||||||
|         loop=loop, |         loop=loop, | ||||||
| @@ -318,6 +369,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | |||||||
|         error_handler=error_handler, |         error_handler=error_handler, | ||||||
|         request_timeout=request_timeout, |         request_timeout=request_timeout, | ||||||
|         request_max_size=request_max_size, |         request_max_size=request_max_size, | ||||||
|  |         request_class=request_class, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     server_coroutine = loop.create_server( |     server_coroutine = loop.create_server( | ||||||
| @@ -379,7 +431,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, | |||||||
|         loop.close() |         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 |     """Start multiple server processes simultaneously.  Stop on interrupt | ||||||
|     and terminate signals, and drain connections when complete. |     and terminate signals, and drain connections when complete. | ||||||
|  |  | ||||||
| @@ -396,19 +448,22 @@ def serve_multiple(server_settings, workers, stop_event=None): | |||||||
|                       " has more information.", DeprecationWarning) |                       " has more information.", DeprecationWarning) | ||||||
|     server_settings['reuse_port'] = True |     server_settings['reuse_port'] = True | ||||||
|  |  | ||||||
|  |     # Handling when custom socket is not provided. | ||||||
|  |     if server_settings.get('sock') is None: | ||||||
|         sock = socket() |         sock = socket() | ||||||
|         sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) |         sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) | ||||||
|         sock.bind((server_settings['host'], server_settings['port'])) |         sock.bind((server_settings['host'], server_settings['port'])) | ||||||
|     set_inheritable(sock.fileno(), True) |         sock.set_inheritable(True) | ||||||
|         server_settings['sock'] = sock |         server_settings['sock'] = sock | ||||||
|         server_settings['host'] = None |         server_settings['host'] = None | ||||||
|         server_settings['port'] = None |         server_settings['port'] = None | ||||||
|  |  | ||||||
|     if stop_event is None: |     def sig_handler(signal, frame): | ||||||
|         stop_event = Event() |         log.info("Received signal {}. Shutting down.".format( | ||||||
|  |             Signals(signal).name)) | ||||||
|  |  | ||||||
|     signal_func(SIGINT, lambda s, f: stop_event.set()) |     signal_func(SIGINT, lambda s, f: sig_handler(s, f)) | ||||||
|     signal_func(SIGTERM, lambda s, f: stop_event.set()) |     signal_func(SIGTERM, lambda s, f: sig_handler(s, f)) | ||||||
|  |  | ||||||
|     processes = [] |     processes = [] | ||||||
|     for _ in range(workers): |     for _ in range(workers): | ||||||
| @@ -423,6 +478,6 @@ def serve_multiple(server_settings, workers, stop_event=None): | |||||||
|     # the above processes will block this until they're stopped |     # the above processes will block this until they're stopped | ||||||
|     for process in processes: |     for process in processes: | ||||||
|         process.terminate() |         process.terminate() | ||||||
|     sock.close() |     server_settings.get('sock').close() | ||||||
|  |  | ||||||
|     asyncio.get_event_loop().stop() |     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 |         # Merge served directory and requested file if provided | ||||||
|         # Strip all / that in the beginning of the URL to help prevent python |         # 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 |         # 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: |         if file_uri: | ||||||
|             file_path = path.join( |             file_path = path.join( | ||||||
|                 file_or_directory, sub('^[/]*', '', file_uri)) |                 file_or_directory, sub('^[/]*', '', file_uri)) | ||||||
|  |  | ||||||
|         # URL decode the path sent by the browser otherwise we won't be able to |         # URL decode the path sent by the browser otherwise we won't be able to | ||||||
|         # match filenames which got encoded (filenames with spaces etc) |         # 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(root_path): | ||||||
|  |             raise FileNotFound('File not found', | ||||||
|  |                                path=file_or_directory, | ||||||
|  |                                relative_url=file_uri) | ||||||
|         try: |         try: | ||||||
|             headers = {} |             headers = {} | ||||||
|             # Check if the client has been sent this file before |             # Check if the client has been sent this file before | ||||||
|   | |||||||
| @@ -1,10 +1,12 @@ | |||||||
|  | import traceback | ||||||
|  |  | ||||||
| from sanic.log import log | from sanic.log import log | ||||||
|  |  | ||||||
| HOST = '127.0.0.1' | HOST = '127.0.0.1' | ||||||
| PORT = 42101 | PORT = 42101 | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestClient: | class SanicTestClient: | ||||||
|     def __init__(self, app): |     def __init__(self, app): | ||||||
|         self.app = app |         self.app = app | ||||||
|  |  | ||||||
| @@ -17,10 +19,15 @@ class TestClient: | |||||||
|                 host=HOST, port=PORT, uri=uri) |                 host=HOST, port=PORT, uri=uri) | ||||||
|  |  | ||||||
|         log.info(url) |         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( |             async with getattr( | ||||||
|                     session, method.lower())(url, *args, **kwargs) as response: |                     session, method.lower())(url, *args, **kwargs) as response: | ||||||
|  |                 try: | ||||||
|                     response.text = await response.text() |                     response.text = await response.text() | ||||||
|  |                 except UnicodeDecodeError as e: | ||||||
|  |                     response.text = None | ||||||
|                 response.body = await response.read() |                 response.body = await response.read() | ||||||
|                 return response |                 return response | ||||||
|  |  | ||||||
| @@ -45,6 +52,8 @@ class TestClient: | |||||||
|                     **request_kwargs) |                     **request_kwargs) | ||||||
|                 results[-1] = response |                 results[-1] = response | ||||||
|             except Exception as e: |             except Exception as e: | ||||||
|  |                 log.error( | ||||||
|  |                     'Exception:\n{}'.format(traceback.format_exc())) | ||||||
|                 exceptions.append(e) |                 exceptions.append(e) | ||||||
|             self.app.stop() |             self.app.stop() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import warnings | import warnings | ||||||
|  |  | ||||||
| from sanic.testing import TestClient | from sanic.testing import SanicTestClient | ||||||
|  |  | ||||||
|  |  | ||||||
| def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, | 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` " |         "the next major version after 0.4.0.  Please use the `test_client` " | ||||||
|         "available on the app object.", DeprecationWarning) |         "available on the app object.", DeprecationWarning) | ||||||
|  |  | ||||||
|     test_client = TestClient(app) |     test_client = SanicTestClient(app) | ||||||
|     return test_client._sanic_endpoint_test( |     return test_client._sanic_endpoint_test( | ||||||
|         method, uri, gather_request, debug, server_kwargs, |         method, uri, gather_request, debug, server_kwargs, | ||||||
|         *request_args, **request_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.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, | ||||||
|  |                 signal=self.signal, | ||||||
|  |                 **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) | ||||||
							
								
								
									
										63
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										63
									
								
								setup.py
									
									
									
									
									
								
							| @@ -4,8 +4,10 @@ Sanic | |||||||
| import codecs | import codecs | ||||||
| import os | import os | ||||||
| import re | 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( | with codecs.open(os.path.join(os.path.abspath(os.path.dirname( | ||||||
|         __file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp: |         __file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp: | ||||||
| @@ -15,27 +17,50 @@ with codecs.open(os.path.join(os.path.abspath(os.path.dirname( | |||||||
|     except IndexError: |     except IndexError: | ||||||
|         raise RuntimeError('Unable to determine version.') |         raise RuntimeError('Unable to determine version.') | ||||||
|  |  | ||||||
| setup( | setup_kwargs = { | ||||||
|     name='sanic', |     'name': 'sanic', | ||||||
|     version=version, |     'version': version, | ||||||
|     url='http://github.com/channelcat/sanic/', |     'url': 'http://github.com/channelcat/sanic/', | ||||||
|     license='MIT', |     'license': 'MIT', | ||||||
|     author='Channel Cat', |     'author': 'Channel Cat', | ||||||
|     author_email='channelcat@gmail.com', |     'author_email': 'channelcat@gmail.com', | ||||||
|     description='A microframework based on uvloop, httptools, and learnings of flask', |     'description': ( | ||||||
|     packages=['sanic'], |         'A microframework based on uvloop, httptools, and learnings of flask'), | ||||||
|     platforms='any', |     'packages': ['sanic'], | ||||||
|     install_requires=[ |     'platforms': 'any', | ||||||
|         'uvloop>=0.5.3;platform_system!="Windows"', |     'classifiers': [ | ||||||
|         'httptools>=0.0.9', |  | ||||||
|         'ujson>=1.35', |  | ||||||
|         'aiofiles>=0.3.0', |  | ||||||
|     ], |  | ||||||
|     classifiers=[ |  | ||||||
|         'Development Status :: 2 - Pre-Alpha', |         'Development Status :: 2 - Pre-Alpha', | ||||||
|         'Environment :: Web Environment', |         'Environment :: Web Environment', | ||||||
|         'License :: OSI Approved :: MIT License', |         'License :: OSI Approved :: MIT License', | ||||||
|         'Programming Language :: Python :: 3.5', |         'Programming Language :: Python :: 3.5', | ||||||
|         'Programming Language :: Python :: 3.6', |         '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 | import inspect | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| @@ -23,6 +24,33 @@ def test_bp(): | |||||||
|  |  | ||||||
|     assert response.text == 'Hello' |     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(): | def test_bp_with_url_prefix(): | ||||||
|     app = Sanic('test_text') |     app = Sanic('test_text') | ||||||
|     bp = Blueprint('test_text', url_prefix='/test1') |     bp = Blueprint('test_text', url_prefix='/test1') | ||||||
| @@ -236,6 +264,7 @@ def test_bp_static(): | |||||||
| def test_bp_shorthand(): | def test_bp_shorthand(): | ||||||
|     app = Sanic('test_shorhand_routes') |     app = Sanic('test_shorhand_routes') | ||||||
|     blueprint = Blueprint('test_shorhand_routes') |     blueprint = Blueprint('test_shorhand_routes') | ||||||
|  |     ev = asyncio.Event() | ||||||
|  |  | ||||||
|     @blueprint.get('/get') |     @blueprint.get('/get') | ||||||
|     def handler(request): |     def handler(request): | ||||||
| @@ -265,6 +294,10 @@ def test_bp_shorthand(): | |||||||
|     def handler(request): |     def handler(request): | ||||||
|         return text('OK') |         return text('OK') | ||||||
|  |  | ||||||
|  |     @blueprint.websocket('/ws') | ||||||
|  |     async def handler(request, ws): | ||||||
|  |         ev.set() | ||||||
|  |  | ||||||
|     app.blueprint(blueprint) |     app.blueprint(blueprint) | ||||||
|  |  | ||||||
|     request, response = app.test_client.get('/get') |     request, response = app.test_client.get('/get') | ||||||
| @@ -308,3 +341,11 @@ def test_bp_shorthand(): | |||||||
|  |  | ||||||
|     request, response = app.test_client.get('/delete') |     request, response = app.test_client.get('/delete') | ||||||
|     assert response.status == 405 |     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 app.config.CONFIG_VALUE == 'should be used' | ||||||
|     assert 'not_for_config' not in app.config |     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(): | def test_load_from_file(): | ||||||
|     app = Sanic('test_load_from_file') |     app = Sanic('test_load_from_file') | ||||||
|   | |||||||
| @@ -44,6 +44,21 @@ def exception_app(): | |||||||
|  |  | ||||||
|     return 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): | def test_no_exception(exception_app): | ||||||
|     """Test that a route works without an exception""" |     """Test that a route works without an exception""" | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import text | from sanic.response import text | ||||||
| from sanic.exceptions import InvalidUsage, ServerError, NotFound | from sanic.exceptions import InvalidUsage, ServerError, NotFound | ||||||
|  | from sanic.handlers import ErrorHandler | ||||||
| from bs4 import BeautifulSoup | from bs4 import BeautifulSoup | ||||||
|  |  | ||||||
| exception_handler_app = Sanic('test_exception_handler') | exception_handler_app = Sanic('test_exception_handler') | ||||||
| @@ -30,7 +31,7 @@ def handler_4(request): | |||||||
| @exception_handler_app.route('/5') | @exception_handler_app.route('/5') | ||||||
| def handler_5(request): | def handler_5(request): | ||||||
|     class CustomServerError(ServerError): |     class CustomServerError(ServerError): | ||||||
|         status_code=200 |         pass | ||||||
|     raise CustomServerError('Custom server error') |     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()) |     summary_text = " ".join(soup.select('.summary')[0].text.split()) | ||||||
|     assert ( |     assert ( | ||||||
|         "NameError: name 'bar' " |         "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(): | def test_inherited_exception_handler(): | ||||||
|     request, response = exception_handler_app.test_client.get('/5') |     request, response = exception_handler_app.test_client.get('/5') | ||||||
|     assert response.status == 200 |     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 import Sanic | ||||||
| from sanic.request import Request | from sanic.request import Request | ||||||
| from sanic.response import json, text, HTTPResponse | 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) |     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(): | def test_middleware_override_request(): | ||||||
|     app = Sanic('test_middleware_override_request') |     app = Sanic('test_middleware_override_request') | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,49 +1,47 @@ | |||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.response import text |  | ||||||
| from sanic.exceptions import PayloadTooLarge | from sanic.exceptions import PayloadTooLarge | ||||||
|  | from sanic.response import text | ||||||
| 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(): | def test_payload_too_large_from_error_handler(): | ||||||
|  |     data_received_app = Sanic('data_received') | ||||||
|  |     data_received_app.config.REQUEST_MAX_SIZE = 1 | ||||||
|  |  | ||||||
|  |     @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) | ||||||
|  |  | ||||||
|     response = data_received_app.test_client.get('/1', gather_request=False) |     response = data_received_app.test_client.get('/1', gather_request=False) | ||||||
|     assert response.status == 413 |     assert response.status == 413 | ||||||
|     assert response.text == 'Payload Too Large from error_handler.' |     assert response.text == 'Payload Too Large from error_handler.' | ||||||
|  |  | ||||||
|  |  | ||||||
| @data_received_default_app.route('/1') | def test_payload_too_large_at_data_received_default(): | ||||||
| async def handler2(request): |     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') |         return text('OK') | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_payload_too_large_at_data_received_default(): |  | ||||||
|     response = data_received_default_app.test_client.get( |     response = data_received_default_app.test_client.get( | ||||||
|         '/1', gather_request=False) |         '/1', gather_request=False) | ||||||
|     assert response.status == 413 |     assert response.status == 413 | ||||||
|     assert response.text == 'Error: Payload Too Large' |     assert response.text == 'Error: Payload Too Large' | ||||||
|  |  | ||||||
|  |  | ||||||
| @on_header_default_app.route('/1') | def test_payload_too_large_at_on_header_default(): | ||||||
| async def handler3(request): |     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') |         return text('OK') | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_payload_too_large_at_on_header_default(): |  | ||||||
|     data = 'a' * 1000 |     data = 'a' * 1000 | ||||||
|     response = on_header_default_app.test_client.post( |     response = on_header_default_app.test_client.post( | ||||||
|         '/1', gather_request=False, data=data) |         '/1', gather_request=False, data=data) | ||||||
|   | |||||||
| @@ -88,4 +88,7 @@ def test_chained_redirect(redirect_app): | |||||||
|     assert request.url.endswith('/1') |     assert request.url.endswith('/1') | ||||||
|     assert response.status == 200 |     assert response.status == 200 | ||||||
|     assert response.text == 'OK' |     assert response.text == 'OK' | ||||||
|  |     try: | ||||||
|         assert response.url.endswith('/3') |         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 json import loads as json_loads, dumps as json_dumps | ||||||
|  | from urllib.parse import urlparse | ||||||
|  | import os | ||||||
|  | import ssl | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| from sanic.exceptions import ServerError | 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('/') |     request, response = app.test_client.get('/') | ||||||
|  |  | ||||||
|     try: |  | ||||||
|     results = json_loads(response.text) |     results = json_loads(response.text) | ||||||
|     except: |  | ||||||
|         raise ValueError("Expected JSON response but got '{}'".format(response)) |  | ||||||
|  |  | ||||||
|     assert results.get('test') == True |     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(): | def test_invalid_json(): | ||||||
|     app = Sanic('test_json') |     app = Sanic('test_json') | ||||||
|  |  | ||||||
|     @app.route('/') |     @app.route('/') | ||||||
|     async def handler(request): |     async def handler(request): | ||||||
|         return json(request.json()) |         return json(request.json) | ||||||
|  |  | ||||||
|     data = "I am not json" |     data = "I am not json" | ||||||
|     request, response = app.test_client.get('/', data=data) |     request, response = app.test_client.get('/', data=data) | ||||||
| @@ -192,3 +205,61 @@ def test_post_form_multipart_form_data(): | |||||||
|     request, response = app.test_client.post(data=payload, headers=headers) |     request, response = app.test_client.post(data=payload, headers=headers) | ||||||
|  |  | ||||||
|     assert request.form.get('test') == 'OK' |     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 random import choice | ||||||
|  |  | ||||||
| from sanic import Sanic | 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(): | def test_response_body_not_a_string(): | ||||||
|     """Test when a response body sent from the application is 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') |     request, response = app.test_client.get('/hello') | ||||||
|     assert response.text == str(random_num) |     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 | import pytest | ||||||
|  |  | ||||||
| from sanic import Sanic | from sanic import Sanic | ||||||
| @@ -22,6 +23,29 @@ def test_shorthand_routes_get(): | |||||||
|     request, response = app.test_client.post('/get') |     request, response = app.test_client.post('/get') | ||||||
|     assert response.status == 405 |     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(): | def test_route_optional_slash(): | ||||||
|     app = Sanic('test_route_optional_slash') |     app = Sanic('test_route_optional_slash') | ||||||
|  |  | ||||||
| @@ -234,6 +258,23 @@ def test_dynamic_route_unhashable(): | |||||||
|     assert response.status == 404 |     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(): | def test_route_duplicate(): | ||||||
|     app = Sanic('test_route_duplicate') |     app = Sanic('test_route_duplicate') | ||||||
|  |  | ||||||
| @@ -498,6 +539,19 @@ def test_remove_inexistent_route(): | |||||||
|     with pytest.raises(RouteDoesNotExist): |     with pytest.raises(RouteDoesNotExist): | ||||||
|         app.remove_route('/test') |         app.remove_route('/test') | ||||||
|  |  | ||||||
|  | def test_removing_slash(): | ||||||
|  |     app = Sanic(__name__) | ||||||
|  |  | ||||||
|  |     @app.get('/rest/<resource>') | ||||||
|  |     def get(_): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     @app.post('/rest/<resource>') | ||||||
|  |     def post(_): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     assert len(app.router.routes_all.keys()) == 2 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_remove_unhashable_route(): | def test_remove_unhashable_route(): | ||||||
|     app = Sanic('test_remove_unhashable_route') |     app = Sanic('test_remove_unhashable_route') | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ def get_file_content(static_file_directory, file_name): | |||||||
|         return file.read() |         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): | def test_static_file(static_file_directory, file_name): | ||||||
|     app = Sanic('test_static') |     app = Sanic('test_static') | ||||||
|     app.static( |     app.static( | ||||||
|   | |||||||
| @@ -15,13 +15,13 @@ def test_methods(method): | |||||||
|  |  | ||||||
|     class DummyView(HTTPMethodView): |     class DummyView(HTTPMethodView): | ||||||
|  |  | ||||||
|         def get(self, request): |         async def get(self, request): | ||||||
|             return text('', headers={'method': 'GET'}) |             return text('', headers={'method': 'GET'}) | ||||||
|  |  | ||||||
|         def post(self, request): |         def post(self, request): | ||||||
|             return text('', headers={'method': 'POST'}) |             return text('', headers={'method': 'POST'}) | ||||||
|  |  | ||||||
|         def put(self, request): |         async def put(self, request): | ||||||
|             return text('', headers={'method': 'PUT'}) |             return text('', headers={'method': 'PUT'}) | ||||||
|  |  | ||||||
|         def head(self, request): |         def head(self, request): | ||||||
| @@ -30,7 +30,7 @@ def test_methods(method): | |||||||
|         def options(self, request): |         def options(self, request): | ||||||
|             return text('', headers={'method': 'OPTIONS'}) |             return text('', headers={'method': 'OPTIONS'}) | ||||||
|  |  | ||||||
|         def patch(self, request): |         async def patch(self, request): | ||||||
|             return text('', headers={'method': 'PATCH'}) |             return text('', headers={'method': 'PATCH'}) | ||||||
|  |  | ||||||
|         def delete(self, request): |         def delete(self, request): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user