Compare commits

...

94 Commits
0.1.5 ... 0.1.8

Author SHA1 Message Date
Eli Uriegas
98b08676e2 Merge pull request #177 from seemethere/increment_version
Increment version to 0.1.8
2016-11-29 15:59:48 -06:00
Eli Uriegas
39f3a63ced Increment version to 0.1.8 2016-11-29 15:59:03 -06:00
Eli Uriegas
89e2084489 Merge pull request #172 from 38elements/timeout
Change request timeout process
2016-11-29 15:56:07 -06:00
Eli Uriegas
cce47a633a Merge pull request #167 from AntonDnepr/class-based-views
Class based views
2016-11-27 21:33:46 -06:00
Eli Uriegas
ec2330c42b Merge pull request #169 from jpiasetz/simplify_imports
Use explicit import for httptools
2016-11-27 20:02:57 -06:00
38elements
ee89b6ad03 before process 2016-11-26 16:47:16 +09:00
38elements
a5e6d6d2e8 Use default error process 2016-11-26 16:02:44 +09:00
Anton Zhyrney
1eea1f5485 rename&remove redundant code 2016-11-26 08:45:08 +02:00
Anton Zhyrney
da4567eea5 changes in doc 2016-11-26 08:44:46 +02:00
38elements
9010a6573f Add status code 2016-11-26 15:21:57 +09:00
38elements
d8e480ab48 Change sleep time 2016-11-26 14:47:42 +09:00
38elements
0bd61f6a57 Use write_response 2016-11-26 14:14:30 +09:00
38elements
c01cbb3a8c Change Request timeout process
This add a request timeout exception.
It cancels task, when request is timeout.
2016-11-26 13:55:45 +09:00
John Piasetzki
0ca5c4eeff Use explicit import for httptools
Explicit importing the parser and the exception to save a name lookup.
2016-11-25 15:14:44 -05:00
Anton Zhyrney
c3c7964e2e pep8 fixes 2016-11-25 09:29:25 +02:00
Anton Zhyrney
fca0221d91 update readme 2016-11-25 09:14:37 +02:00
Anton Zhyrney
9f2d73e2f1 class based views implementation for sanic 2016-11-25 09:10:25 +02:00
Eli Uriegas
fc19f2ea34 Merge pull request #163 from channelcat/request-data-vars
Access Request like a dictionary

Closes #129 #132
2016-11-23 13:51:17 -06:00
Eli Uriegas
aa0f15fbb2 Adding a new line 2016-11-23 11:03:00 -06:00
Eli Uriegas
93f50b8ef7 Merge pull request #160 from jiajunhuang/log
fix the way using logging.exception
2016-11-21 10:37:10 -06:00
Eli Uriegas
7b85843363 Merge pull request #165 from abhishek7/master
Revised error message in server.py (Issue #157)
2016-11-21 10:17:02 -06:00
abhishek7
f7f578ed44 Fixed Exception error log on line 157 of server.py 2016-11-20 21:37:01 -05:00
Abhishek
de92603ccf Merge pull request #3 from channelcat/master
Updating base fork
2016-11-20 21:28:33 -05:00
Channel Cat
d02fffb6b8 Fixing import of CIMultiDict 2016-11-19 18:41:40 -08:00
Channel Cat
922c96e3c1 Updated test terminology 2016-11-19 18:26:03 -08:00
Channel Cat
993627ec44 Merged with master 2016-11-19 18:21:44 -08:00
Channel Cat
01681599ff Fixed new test error with aiohttp 2016-11-19 18:13:02 -08:00
Channel Cat
3ce6434532 Fix flake8 2016-11-19 18:04:35 -08:00
Channel Cat
a97e554f8f Added shared request data 2016-11-19 17:48:28 -08:00
Eli Uriegas
fd5a79a685 Merge pull request #162 from r0fls/ciheaders
Update headers to use CIMultiDict instead of dict.
2016-11-19 18:32:26 -06:00
Raphael Deem
635921adc7 Update headers to use CIMultiDict instead of dict 2016-11-19 16:09:38 -08:00
jiajunhuang
9eb4cecbc1 fix the way using logging.exception 2016-11-19 15:19:38 +08:00
Eli Uriegas
879b9a4a15 Merge pull request #159 from r0fls/namedefault
provide default app name
2016-11-18 20:47:50 -06:00
Raphael Deem
8be4dc8fb5 update readme example to use default 2016-11-18 17:22:24 -08:00
Raphael Deem
f16ea20de5 provide default app name 2016-11-18 17:16:48 -08:00
Eli Uriegas
c51b14856e Merge pull request #154 from jackfischer/master
Example for using error_handler
2016-11-16 13:03:20 -06:00
Eli Uriegas
88ee71c425 Merge pull request #155 from seemethere/fix_flake8_errors
Fix the flake8 error caused by new flake8 version
2016-11-16 12:58:27 -06:00
Eli Uriegas
edb12da154 Fix the flake8 error caused by new flake8 version 2016-11-16 12:55:13 -06:00
Jack Fischer
d9f6846c76 improved default handling 2016-11-16 07:55:54 -05:00
Jack Fischer
9e0747db15 Example for using error_handler 2016-11-15 19:37:40 -05:00
Eli Uriegas
ae3d33ad58 Merge pull request #149 from asvetlov/patch-1
aiohttp is slightly faster actually
2016-11-14 10:24:15 -06:00
Manuel Miranda
edb25f799d Caching example (#150)
* Caching example using aiocache

* Caching example using aiocache

* Added aiocache to requirements

* Fixed example with newest aiocache
2016-11-13 17:11:31 -06:00
Andrew Svetlov
0822674f70 aiohttp is slightly faster actually
Disabling access log increases RPS a lot
2016-11-11 22:36:49 +02:00
Eli Uriegas
49d004736a Merge pull request #148 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2016-11-10 22:12:09 -06:00
The Gitter Badger
695f8733bb Add Gitter badge 2016-11-11 04:11:07 +00:00
Eli Uriegas
b51af7f4bf Merge pull request #147 from webtic/master
Add the client address to the request header
2016-11-10 15:30:04 -06:00
Eli Uriegas
28ce2447ef Update variable name
Give `ra` a more explicit name
2016-11-10 15:28:16 -06:00
Eli Uriegas
42e3a50274 Merge pull request #145 from pahaz/fix-request-parse-multipart-form
Fix request parse multipart form
2016-11-10 09:00:06 -06:00
Paul Jongsma
8ebc92c236 pass flake8 tests 2016-11-10 13:09:37 +01:00
Paul Jongsma
b92e46df40 fix whitespace 2016-11-10 13:06:27 +01:00
Paul Jongsma
be5588d5d8 Add the client address to the request header 2016-11-10 12:53:00 +01:00
Pahaz Blinov
0d9fb2f927 docs(request): return value docstring 2016-11-09 18:04:15 +05:00
Pahaz Blinov
0e9819fba1 fix(request): parse_multipart_form should return RequestParameters
I have this code:

```
form = FileForm(request.files)
```

and it raise error because the `request.files` is `dict` but `RequestParameters` is expected =/
2016-11-09 00:36:37 +05:00
Pahaz Blinov
aaee40aabd Merge pull request #6 from channelcat/master
fix(request.py): problem in case of request without content-type head…
2016-11-09 00:30:39 +05:00
Pahaz Blinov
5efe51b661 fix(request.py): problem in case of request without content-type header (#142)
* fix(request.py): exception if access request.form on GET request

* fix(request): just make a unification (parsed_form and parsed_files) + RFC fixes

parsed_form and parsed_files must be a RequestParameters type in all cases!
2016-11-07 15:27:50 -06:00
Pahaz Blinov
50f63142db Merge pull request #4 from channelcat/master
pull from master
2016-11-06 21:35:20 +05:00
Pahaz Blinov
1b65b2e0c6 fix(blueprints): @middleware IndexError (#139) 2016-11-06 10:08:55 -06:00
Manuel Miranda
ce8742c605 Caching example using aiocache (#140)
* Keep-alive requests stay open if communicating

* time.time faster than loop.time?

* Fix flake8

* Add aiofiles to requirements.txt

* Caching example using aiocache

* Caching example using aiocache

* Added aiocache to requirements
2016-11-06 09:26:15 -06:00
Eli Uriegas
01a013b48a Merge pull request #141 from seemethere/fix_query_tests
Fix value error for query string test
2016-11-05 12:16:13 -06:00
Eli Uriegas
3a2eeb9709 Fix value error for query string test 2016-11-05 13:12:55 -05:00
Eli Uriegas
1271c7d958 Merge pull request #128 from channelcat/keep-alive-timeout-fix
Keep alive timeout fix
2016-11-05 12:11:41 -06:00
Eli Uriegas
0032f525ce Merge pull request #138 from seemethere/add_aiofiles_requirements
Add aiofiles to requirements.txt
2016-11-05 12:11:08 -06:00
Eli Uriegas
df2f91b82f Add aiofiles to requirements.txt 2016-11-03 09:35:14 -05:00
Eli Uriegas
3a1ef6bef2 Merge pull request #125 from clenimar/fix-comments
Fix comments over-indentation
2016-11-03 08:27:39 -06:00
Eli Uriegas
28488075b9 Merge pull request #137 from imbolc/upload-without-content-type
Fix upload without content-type
2016-11-03 08:26:05 -06:00
imbolc
3cd3b2d9b7 Fix upload without content-type 2016-11-03 12:34:55 +07:00
Channel Cat
3d88818841 Merge pull request #135 from Sharpek/master
Add loop kwargs to sanic_endpoint_test
2016-11-02 21:37:52 -07:00
Channel Cat
b74cf65eca Merge pull request #134 from RyanKung/patch-1
Update README.md
2016-11-02 21:28:36 -07:00
Marcin Baran
80fcacaf8b Add loop kwargs to sanic_endpoint_test 2016-11-02 12:27:58 +01:00
Ryan Kung
96fcd8443f Update README.md
via flake8
2016-11-01 14:35:06 +08:00
Channel Cat
707c55fbe7 Fix flake8 2016-10-28 03:35:30 -07:00
Channel Cat
c44b5551bc time.time faster than loop.time? 2016-10-28 03:13:03 -07:00
Channel Cat
bd28da0abc Keep-alive requests stay open if communicating 2016-10-28 02:56:32 -07:00
Abhishek
410299f5a1 Merge pull request #2 from channelcat/master
Updates from Oct 17th
2016-10-27 20:06:56 -04:00
Clenimar Filemon
f3fc958a0c Fix comments over-indentation 2016-10-27 11:09:36 -03:00
Channel Cat
47b417db28 Merge pull request #119 from jackfischer/master
Add example with async http requests (Issue #96)
2016-10-26 22:43:01 -07:00
Jack Fischer
5171cdd305 add example with async http requests 2016-10-26 16:53:34 -04:00
Channel Cat
65950250d9 Merge pull request #111 from channelcat/reverse-static
Reverse static arguments
2016-10-25 02:52:21 -07:00
Channel Cat
74ae0007d3 Reverse static arguments 2016-10-25 02:45:28 -07:00
Channel Cat
977081f4af Merge pull request #110 from channelcat/cookies-lazy-creation
Lazy cookie creation
2016-10-25 01:50:33 -07:00
Channel Cat
ee70f1e55e Upped to version 0.1.6 2016-10-25 01:49:43 -07:00
Channel Cat
9c16f6dbea Fix flake8 issues 2016-10-25 01:36:12 -07:00
Channel Cat
c50aa34dd9 Lazy cookie creation 2016-10-25 01:27:54 -07:00
Channel Cat
0e479d53da Merge pull request #104 from channelcat/pr/101
Static file support
2016-10-24 22:42:01 -07:00
Channel Cat
984c086296 Merge pull request #105 from channelcat/blueprint-ordering
Added blueprint order test and used deques to add blueprints
2016-10-24 02:45:31 -07:00
Channel Cat
53e00b2b4c Added blueprint order test and used deques to add blueprints 2016-10-24 02:09:07 -07:00
Channel Cat
bb1cb29edd Merge pull request #103 from pcdinh/master
Made Pylint happy: clean up some unused variables
2016-10-24 01:22:06 -07:00
Channel Cat
bf6879e46f Made static file serving part of Sanic
Added sanic.static, blueprint.static, documentation, and testing
2016-10-24 01:21:06 -07:00
Channel Cat
12e900e8f9 Merge pull request #100 from chhsiao90/test-router
Add test for method not allow situation
2016-10-23 21:15:31 -07:00
imbolc
d7fff12b71 Static middleware 2016-10-24 02:17:03 +07:00
chhsiao90
9051e985a0 Add test for method not allow situation 2016-10-23 21:58:57 +08:00
pcdinh
5361c6f243 e is an unused variable. Safe to remove 2016-10-23 19:38:28 +07:00
pcdinh
963aef19e0 w is unused variable to it is safe to suppress Pylint warning using _
(underscore)
2016-10-23 19:36:08 +07:00
Abhishek
f95fe4192b Merge pull request #1 from channelcat/master
Updating the fork
2016-10-17 19:36:13 -04:00
34 changed files with 1216 additions and 80 deletions

View File

@@ -1,5 +1,10 @@
Version 0.1
-----------
- 0.1.7
- Reversed static url and directory arguments to meet spec
- 0.1.6
- Static files
- Lazy Cookie Loading
- 0.1.5
- Cookies
- Blueprint listeners and ordering

View File

@@ -1,5 +1,7 @@
# Sanic
[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/)
[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/)
@@ -31,11 +33,11 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
app = Sanic()
@app.route("/")
async def test(request):
return json({ "hello": "world" })
return json({"hello": "world"})
app.run(host="0.0.0.0", port=8000)
```
@@ -50,7 +52,9 @@ app.run(host="0.0.0.0", port=8000)
* [Middleware](docs/middleware.md)
* [Exceptions](docs/exceptions.md)
* [Blueprints](docs/blueprints.md)
* [Class Based Views](docs/class_based_views.md)
* [Cookies](docs/cookies.md)
* [Static Files](docs/static_files.md)
* [Deploying](docs/deploying.md)
* [Contributing](docs/contributing.md)
* [License](LICENSE)
@@ -69,7 +73,7 @@ app.run(host="0.0.0.0", port=8000)
▄▄▄▄▄
▀▀▀██████▄▄▄ _______________
▄▄▄▄▄ █████████▄ / \
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
▀▀█████▄▄ ▀██████▄██ | _________________/
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
▀▀▀▄ ▀▀███ ▀ ▄▄

View File

@@ -42,7 +42,7 @@ from sanic import Sanic
from my_blueprint import bp
app = Sanic(__name__)
app.register_blueprint(bp)
app.blueprint(bp)
app.run(host='0.0.0.0', port=8000, debug=True)
```
@@ -79,6 +79,12 @@ Exceptions can also be applied exclusively to blueprints globally.
@bp.exception(NotFound)
def ignore_404s(request, exception):
return text("Yep, I totally found the page: {}".format(request.url))
## Static files
Static files can also be served globally, under the blueprint prefix.
```python
bp.static('/folder/to/serve', '/web/path')
```
## Start and Stop

44
docs/class_based_views.md Normal file
View File

@@ -0,0 +1,44 @@
# Class based views
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response.
## Examples
```python
from sanic import Sanic
from sanic.views import HTTPMethodView
app = Sanic('some_name')
class SimpleView(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(SimpleView(), '/')
```
If you need any url params just mention them in method definition:
```python
class NameView(HTTPMethodView):
def get(self, request, name):
return text('Hello {}'.format(name))
app.add_route(NameView(), '/<name')
```

View File

@@ -29,4 +29,16 @@ async def person_handler(request, name):
async def folder_handler(request, folder_id):
return text('Folder - {}'.format(folder_id))
async def handler1(request):
return text('OK')
app.add_route(handler1, '/test')
async def handler(request, name):
return text('Folder - {}'.format(name))
app.add_route(handler, '/folder/<name>')
async def person_handler(request, name):
return text('Person - {}'.format(name))
app.add_route(handler, '/person/<name:[A-z]>')
```

18
docs/static_files.md Normal file
View File

@@ -0,0 +1,18 @@
# Static Files
Both directories and files can be served by registering with static
## Example
```python
app = Sanic(__name__)
# Serves files from the static folder to the URL /static
app.static('/static', './static')
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
# is requested
app.static('/the_best.png', '/home/ubuntu/test.png')
app.run(host="0.0.0.0", port=8000)
```

View File

@@ -0,0 +1,33 @@
from sanic import Sanic
from sanic.response import json
import uvloop
import aiohttp
#Create an event loop manually so that we can use it for both sanic & aiohttp
loop = uvloop.new_event_loop()
app = Sanic(__name__)
async def fetch(session, url):
"""
Use session object to perform 'get' request on url
"""
async with session.get(url) as response:
return await response.json()
@app.route("/")
async def test(request):
"""
Download and serve example JSON
"""
url = "https://api.github.com/repos/channelcat/sanic"
async with aiohttp.ClientSession(loop=loop) as session:
response = await fetch(session, url)
return json(response)
app.run(host="0.0.0.0", port=8000, loop=loop)

41
examples/cache_example.py Normal file
View File

@@ -0,0 +1,41 @@
"""
Example of caching using aiocache package. To run it you will need a Redis
instance running in localhost:6379.
Running this example you will see that the first call lasts 3 seconds and
the rest are instant because the value is retrieved from the Redis.
If you want more info about the package check
https://github.com/argaen/aiocache
"""
import asyncio
import aiocache
from sanic import Sanic
from sanic.response import json
from sanic.log import log
from aiocache import cached
from aiocache.serializers import JsonSerializer
app = Sanic(__name__)
aiocache.settings.set_defaults(
cache="aiocache.RedisCache"
)
@cached(key="my_custom_key", serializer=JsonSerializer())
async def expensive_call():
log.info("Expensive has been called")
await asyncio.sleep(3)
return {"test": True}
@app.route("/")
async def test(request):
log.info("Received GET /")
return json(await expensive_call())
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop())

View File

@@ -0,0 +1,60 @@
"""
Example intercepting uncaught exceptions using Sanic's error handler framework.
This may be useful for developers wishing to use Sentry, Airbrake, etc.
or a custom system to log and monitor unexpected errors in production.
First we create our own class inheriting from Handler in sanic.exceptions,
and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to
an external service.
"""
"""
Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file)
"""
from sanic.response import text
from sanic.exceptions import Handler, SanicException
class CustomHandler(Handler):
def default(self, request, exception):
# Here, we have access to the exception object
# and can do anything with it (log, send to external service, etc)
# Some exceptions are trivial and built into Sanic (404s, etc)
if not issubclass(type(exception), SanicException):
print(exception)
# Then, we must finish handling the exception by returning
# our response to the client
# For this we can just call the super class' default handler
return super.default(self, request, exception)
"""
This is an ordinary Sanic server, with the exception that we set the
server's error_handler to an instance of our CustomHandler
"""
from sanic import Sanic
from sanic.response import json
app = Sanic(__name__)
handler = CustomHandler(sanic=app)
app.error_handler = handler
@app.route("/")
async def test(request):
# Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler.
x = 1 / 0
return json({"test": True})
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -0,0 +1,21 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.config import Config
from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1
app = Sanic(__name__)
@app.route('/')
async def test(request):
await asyncio.sleep(3)
return text('Hello, world!')
@app.exception(RequestTimeout)
def timeout(request, exception):
return text('RequestTimeout from error_handler.', 408)
app.run(host='0.0.0.0', port=8000)

View File

@@ -2,6 +2,7 @@ httptools
ujson
uvloop
aiohttp
aiocache
pytest
coverage
tox
@@ -9,4 +10,5 @@ gunicorn
bottle
kyoukai
falcon
tornado
tornado
aiofiles

View File

@@ -1,3 +1,5 @@
httptools
ujson
uvloop
uvloop
aiofiles
multidict

View File

@@ -1,4 +1,6 @@
from .sanic import Sanic
from .blueprints import Blueprint
__version__ = '0.1.8'
__all__ = ['Sanic', 'Blueprint']

View File

@@ -33,6 +33,15 @@ class BlueprintSetup:
"""
self.app.exception(*args, **kwargs)(handler)
def add_static(self, uri, file_or_directory, *args, **kwargs):
"""
Registers static files to sanic
"""
if self.url_prefix:
uri = self.url_prefix + uri
self.app.static(uri, file_or_directory, *args, **kwargs)
def add_middleware(self, middleware, *args, **kwargs):
"""
Registers middleware to sanic
@@ -82,6 +91,12 @@ class Blueprint:
return handler
return decorator
def add_route(self, handler, uri, methods=None):
"""
"""
self.record(lambda s: s.add_route(handler, uri, methods))
return handler
def listener(self, event):
"""
"""
@@ -100,8 +115,9 @@ class Blueprint:
# Detect which way this was called, @middleware or @middleware('AT')
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
middleware = args[0]
args = []
return register_middleware(args[0])
return register_middleware(middleware)
else:
return register_middleware
@@ -112,3 +128,9 @@ class Blueprint:
self.record(lambda s: s.add_exception(handler, *args, **kwargs))
return handler
return decorator
def static(self, uri, file_or_directory, *args, **kwargs):
"""
"""
self.record(
lambda s: s.add_static(uri, file_or_directory, *args, **kwargs))

130
sanic/cookies.py Normal file
View File

@@ -0,0 +1,130 @@
from datetime import datetime
import re
import string
# ------------------------------------------------------------ #
# SimpleCookie
# ------------------------------------------------------------ #
# Straight up copied this section of dark magic from SimpleCookie
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:"
_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}'
_Translator = {n: '\\%03o' % n
for n in set(range(256)) - set(map(ord, _UnescapedChars))}
_Translator.update({
ord('"'): '\\"',
ord('\\'): '\\\\',
})
def _quote(str):
r"""Quote a string for use in a cookie header.
If the string does not need to be double-quoted, then just return the
string. Otherwise, surround the string in doublequotes and quote
(with a \) special characters.
"""
if str is None or _is_legal_key(str):
return str
else:
return '"' + str.translate(_Translator) + '"'
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
# ------------------------------------------------------------ #
# Custom SimpleCookie
# ------------------------------------------------------------ #
class CookieJar(dict):
"""
CookieJar dynamically writes headers as cookies are added and removed
It gets around the limitation of one header per name by using the
MultiHeader class to provide a unique key that encodes to Set-Cookie
"""
def __init__(self, headers):
super().__init__()
self.headers = headers
self.cookie_headers = {}
def __setitem__(self, key, value):
# If this cookie doesn't exist, add it to the header keys
cookie_header = self.cookie_headers.get(key)
if not cookie_header:
cookie = Cookie(key, value)
cookie_header = MultiHeader("Set-Cookie")
self.cookie_headers[key] = cookie_header
self.headers[cookie_header] = cookie
return super().__setitem__(key, cookie)
else:
self[key].value = value
def __delitem__(self, key):
del self.cookie_headers[key]
return super().__delitem__(key)
class Cookie(dict):
"""
This is a stripped down version of Morsel from SimpleCookie #gottagofast
"""
_keys = {
"expires": "expires",
"path": "Path",
"comment": "Comment",
"domain": "Domain",
"max-age": "Max-Age",
"secure": "Secure",
"httponly": "HttpOnly",
"version": "Version",
}
_flags = {'secure', 'httponly'}
def __init__(self, key, value):
if key in self._keys:
raise KeyError("Cookie name is a reserved word")
if not _is_legal_key(key):
raise KeyError("Cookie key contains illegal characters")
self.key = key
self.value = value
super().__init__()
def __setitem__(self, key, value):
if key not in self._keys:
raise KeyError("Unknown cookie property")
return super().__setitem__(key, value)
def encode(self, encoding):
output = ['%s=%s' % (self.key, _quote(self.value))]
for key, value in self.items():
if key == 'max-age' and isinstance(value, int):
output.append('%s=%d' % (self._keys[key], value))
elif key == 'expires' and isinstance(value, datetime):
output.append('%s=%s' % (
self._keys[key],
value.strftime("%a, %d-%b-%Y %T GMT")
))
elif key in self._flags:
output.append(self._keys[key])
else:
output.append('%s=%s' % (self._keys[key], value))
return "; ".join(output).encode(encoding)
# ------------------------------------------------------------ #
# Header Trickery
# ------------------------------------------------------------ #
class MultiHeader:
"""
Allows us to set a header within response that has a unique key,
but may contain duplicate header names
"""
def __init__(self, name):
self.name = name
def encode(self):
return self.name.encode()

View File

@@ -21,6 +21,19 @@ class ServerError(SanicException):
status_code = 500
class FileNotFound(NotFound):
status_code = 404
def __init__(self, message, path, relative_url):
super().__init__(message)
self.path = path
self.relative_url = relative_url
class RequestTimeout(SanicException):
status_code = 408
class Handler:
handlers = None

View File

@@ -8,6 +8,12 @@ from ujson import loads as json_loads
from .log import log
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
# > If the media type remains unknown, the recipient SHOULD treat it
# > as type "application/octet-stream"
class RequestParameters(dict):
"""
Hosts a dict with lists as values where get returns the first
@@ -26,7 +32,7 @@ class RequestParameters(dict):
return self.super.get(name, default)
class Request:
class Request(dict):
"""
Properties of an HTTP request such as URL, headers, etc.
"""
@@ -61,21 +67,20 @@ class Request:
try:
self.parsed_json = json_loads(self.body)
except Exception:
pass
log.exception("failed when parsing body as json")
return self.parsed_json
@property
def form(self):
if self.parsed_form is None:
self.parsed_form = {}
self.parsed_files = {}
content_type, parameters = parse_header(
self.headers.get('Content-Type'))
self.parsed_form = RequestParameters()
self.parsed_files = RequestParameters()
content_type = self.headers.get(
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
content_type, parameters = parse_header(content_type)
try:
is_url_encoded = (
content_type == 'application/x-www-form-urlencoded')
if content_type is None or is_url_encoded:
if content_type == 'application/x-www-form-urlencoded':
self.parsed_form = RequestParameters(
parse_qs(self.body.decode('utf-8')))
elif content_type == 'multipart/form-data':
@@ -83,9 +88,8 @@ class Request:
boundary = parameters['boundary'].encode('utf-8')
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception as e:
log.exception(e)
pass
except Exception:
log.exception("failed when parsing form")
return self.parsed_form
@@ -128,10 +132,10 @@ def parse_multipart_form(body, boundary):
Parses a request body and returns fields and files
:param body: Bytes request body
:param boundary: Bytes multipart boundary
:return: fields (dict), files (dict)
:return: fields (RequestParameters), files (RequestParameters)
"""
files = {}
fields = {}
files = RequestParameters()
fields = RequestParameters()
form_parts = body.split(boundary)
for form_part in form_parts[1:-1]:
@@ -162,9 +166,16 @@ def parse_multipart_form(body, boundary):
post_data = form_part[line_index:-4]
if file_name or file_type:
files[field_name] = File(
type=file_type, name=file_name, body=post_data)
file = File(type=file_type, name=file_name, body=post_data)
if field_name in files:
files[field_name].append(file)
else:
files[field_name] = [file]
else:
fields[field_name] = post_data.decode('utf-8')
value = post_data.decode('utf-8')
if field_name in fields:
fields[field_name].append(value)
else:
fields[field_name] = [value]
return fields, files

View File

@@ -1,6 +1,8 @@
from datetime import datetime
from http.cookies import SimpleCookie
import ujson
from aiofiles import open as open_async
from .cookies import CookieJar
from mimetypes import guess_type
from os import path
from ujson import dumps as json_dumps
COMMON_STATUS_CODES = {
200: b'OK',
@@ -98,12 +100,6 @@ class HTTPResponse:
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
for name, value in self.headers.items()
)
if self._cookies:
for cookie in self._cookies.values():
if type(cookie['expires']) is datetime:
cookie['expires'] = \
cookie['expires'].strftime("%a, %d-%b-%Y %T GMT")
headers += (str(self._cookies) + "\r\n").encode('utf-8')
# Try to pull from the common codes first
# Speeds up response rate 6% over pulling from all
@@ -131,12 +127,12 @@ class HTTPResponse:
@property
def cookies(self):
if self._cookies is None:
self._cookies = SimpleCookie()
self._cookies = CookieJar(self.headers)
return self._cookies
def json(body, status=200, headers=None):
return HTTPResponse(ujson.dumps(body), headers=headers, status=status,
return HTTPResponse(json_dumps(body), headers=headers, status=status,
content_type="application/json")
@@ -148,3 +144,17 @@ def text(body, status=200, headers=None):
def html(body, status=200, headers=None):
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None):
filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file:
out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=200,
headers=headers,
content_type=mime_type,
body_bytes=out_stream)

View File

@@ -82,6 +82,9 @@ class Router:
# Mark the whole route as unhashable if it has the hash key in it
if re.search('(^|[^^]){1}/', pattern):
properties['unhashable'] = True
# Mark the route as unhashable if it matches the hash key
elif re.search(pattern, '/'):
properties['unhashable'] = True
return '({})'.format(pattern)

View File

@@ -1,6 +1,7 @@
from asyncio import get_event_loop
from collections import deque
from functools import partial
from inspect import isawaitable
from inspect import isawaitable, stack, getmodulename
from multiprocessing import Process, Event
from signal import signal, SIGTERM, SIGINT
from time import sleep
@@ -12,22 +13,29 @@ from .log import log, logging
from .response import HTTPResponse
from .router import Router
from .server import serve
from .static import register as static_register
from .exceptions import ServerError
class Sanic:
def __init__(self, name, router=None, error_handler=None):
def __init__(self, name=None, router=None, error_handler=None):
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
self.name = name
self.router = router or Router()
self.error_handler = error_handler or Handler(self)
self.config = Config()
self.request_middleware = []
self.response_middleware = []
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
self._blueprint_order = []
self.loop = None
self.debug = None
# Register alternative method names
self.go_fast = self.run
# -------------------------------------------------------------------- #
# Registration
# -------------------------------------------------------------------- #
@@ -41,12 +49,30 @@ class Sanic:
:return: decorated function
"""
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith('/'):
uri = '/' + uri
def response(handler):
self.router.add(uri=uri, methods=methods, handler=handler)
return handler
return response
def add_route(self, handler, uri, methods=None):
"""
A helper method to register class instance or
functions as a handler to the application url
routes.
:param handler: function or class instance
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:return: function or class instance
"""
self.route(uri=uri, methods=methods)(handler)
return handler
# Decorator
def exception(self, *exceptions):
"""
@@ -74,7 +100,7 @@ class Sanic:
if attach_to == 'request':
self.request_middleware.append(middleware)
if attach_to == 'response':
self.response_middleware.insert(0, middleware)
self.response_middleware.appendleft(middleware)
return middleware
# Detect which way this was called, @middleware or @middleware('AT')
@@ -84,7 +110,17 @@ class Sanic:
attach_to = args[0]
return register_middleware
def register_blueprint(self, blueprint, **options):
# Static Files
def static(self, uri, file_or_directory, pattern='.+',
use_modified_since=True):
"""
Registers a root to serve files from. The input can either be a file
or a directory. See
"""
static_register(self, uri, file_or_directory, pattern,
use_modified_since)
def blueprint(self, blueprint, **options):
"""
Registers a blueprint on the application.
:param blueprint: Blueprint object
@@ -101,6 +137,12 @@ class Sanic:
self._blueprint_order.append(blueprint)
blueprint.register(self, options)
def register_blueprint(self, *args, **kwargs):
# TODO: deprecate 1.0
log.warning("Use of register_blueprint will be deprecated in "
"version 1.0. Please use the blueprint method instead")
return self.blueprint(*args, **kwargs)
# -------------------------------------------------------------------- #
# Request Handling
# -------------------------------------------------------------------- #
@@ -221,6 +263,7 @@ class Sanic:
'sock': sock,
'debug': debug,
'request_handler': self.handle_request,
'error_handler': self.error_handler,
'request_timeout': self.config.REQUEST_TIMEOUT,
'request_max_size': self.config.REQUEST_MAX_SIZE,
'loop': loop
@@ -266,8 +309,7 @@ class Sanic:
except Exception as e:
log.exception(
'Experienced exception while trying to serve: {}'.format(e))
pass
'Experienced exception while trying to serve')
log.info("Server Stopped")
@@ -296,7 +338,7 @@ class Sanic:
signal(SIGTERM, lambda s, f: stop_event.set())
processes = []
for w in range(workers):
for _ in range(workers):
process = Process(target=serve, kwargs=server_settings)
process.start()
processes.append(process)

View File

@@ -1,8 +1,11 @@
import asyncio
from functools import partial
from inspect import isawaitable
from multidict import CIMultiDict
from signal import SIGINT, SIGTERM
import httptools
from time import time
from httptools import HttpRequestParser
from httptools.parser.errors import HttpParserError
try:
import uvloop as async_loop
@@ -11,12 +14,16 @@ except ImportError:
from .log import log
from .request import Request
from .exceptions import RequestTimeout
class Signal:
stopped = False
current_time = None
class HttpProtocol(asyncio.Protocol):
__slots__ = (
# event loop, connection
@@ -26,10 +33,10 @@ class HttpProtocol(asyncio.Protocol):
# request config
'request_handler', 'request_timeout', 'request_max_size',
# connection management
'_total_request_size', '_timeout_handler')
'_total_request_size', '_timeout_handler', '_last_communication_time')
def __init__(self, *, loop, request_handler, signal=Signal(),
connections={}, request_timeout=60,
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections={}, request_timeout=60,
request_max_size=None):
self.loop = loop
self.transport = None
@@ -40,13 +47,15 @@ class HttpProtocol(asyncio.Protocol):
self.signal = signal
self.connections = connections
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
self.request_max_size = request_max_size
self._total_request_size = 0
self._timeout_handler = None
self._last_request_time = None
self._request_handler_task = None
# -------------------------------------------- #
# -------------------------------------------- #
# Connection
# -------------------------------------------- #
@@ -55,6 +64,7 @@ class HttpProtocol(asyncio.Protocol):
self._timeout_handler = self.loop.call_later(
self.request_timeout, self.connection_timeout)
self.transport = transport
self._last_request_time = current_time
def connection_lost(self, exc):
del self.connections[self]
@@ -62,10 +72,20 @@ class HttpProtocol(asyncio.Protocol):
self.cleanup()
def connection_timeout(self):
self.bail_out("Request timed out, connection closed")
# -------------------------------------------- #
# Check if
time_elapsed = current_time - self._last_request_time
if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed
self._timeout_handler = \
self.loop.call_later(time_left, self.connection_timeout)
else:
if self._request_handler_task:
self._request_handler_task.cancel()
response = self.error_handler.response(
self.request, RequestTimeout('Request Timeout'))
self.write_response(response)
# -------------------------------------------- #
# Parsing
# -------------------------------------------- #
@@ -82,12 +102,12 @@ class HttpProtocol(asyncio.Protocol):
if self.parser is None:
assert self.request is None
self.headers = []
self.parser = httptools.HttpRequestParser(self)
self.parser = HttpRequestParser(self)
# Parse request chunk or close connection
try:
self.parser.feed_data(data)
except httptools.parser.errors.HttpParserError as e:
except HttpParserError as e:
self.bail_out(
"Invalid request data, connection closed ({})".format(e))
@@ -102,9 +122,13 @@ class HttpProtocol(asyncio.Protocol):
self.headers.append((name.decode(), value.decode('utf-8')))
def on_headers_complete(self):
remote_addr = self.transport.get_extra_info('peername')
if remote_addr:
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
self.request = Request(
url_bytes=self.url,
headers=dict(self.headers),
headers=CIMultiDict(self.headers),
version=self.parser.get_http_version(),
method=self.parser.get_method().decode()
)
@@ -116,7 +140,7 @@ class HttpProtocol(asyncio.Protocol):
self.request.body = body
def on_message_complete(self):
self.loop.create_task(
self._request_handler_task = self.loop.create_task(
self.request_handler(self.request, self.write_response))
# -------------------------------------------- #
@@ -133,13 +157,15 @@ class HttpProtocol(asyncio.Protocol):
if not keep_alive:
self.transport.close()
else:
# Record that we received data
self._last_request_time = current_time
self.cleanup()
except Exception as e:
self.bail_out(
"Writing request failed, connection closed {}".format(e))
"Writing response failed, connection closed {}".format(e))
def bail_out(self, message):
log.error(message)
log.debug(message)
self.transport.close()
def cleanup(self):
@@ -147,6 +173,7 @@ class HttpProtocol(asyncio.Protocol):
self.request = None
self.url = None
self.headers = None
self._request_handler_task = None
self._total_request_size = 0
def close_if_idle(self):
@@ -160,6 +187,18 @@ class HttpProtocol(asyncio.Protocol):
return False
def update_current_time(loop):
"""
Caches the current time, since it is needed
at the end of every keep-alive request to update the request timeout time
:param loop:
:return:
"""
global current_time
current_time = time()
loop.call_later(1, partial(update_current_time, loop))
def trigger_events(events, loop):
"""
:param events: one or more sync or async functions to execute
@@ -174,8 +213,8 @@ def trigger_events(events, loop):
loop.run_until_complete(result)
def serve(host, port, request_handler, before_start=None, after_start=None,
before_stop=None, after_stop=None,
def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None,
debug=False, request_timeout=60, sock=None,
request_max_size=None, reuse_port=False, loop=None):
"""
@@ -210,13 +249,18 @@ def serve(host, port, request_handler, before_start=None, after_start=None,
connections=connections,
signal=signal,
request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout,
request_max_size=request_max_size,
), host, port, reuse_port=reuse_port, sock=sock)
# Instead of pulling time at the end of every request,
# pull it once per minute
loop.call_soon(partial(update_current_time, loop))
try:
http_server = loop.run_until_complete(server_coroutine)
except Exception as e:
except Exception:
log.exception("Unable to start server")
return

59
sanic/static.py Normal file
View File

@@ -0,0 +1,59 @@
from aiofiles.os import stat
from os import path
from re import sub
from time import strftime, gmtime
from .exceptions import FileNotFound, InvalidUsage
from .response import file, HTTPResponse
def register(app, uri, file_or_directory, pattern, use_modified_since):
# TODO: Though sanic is not a file server, I feel like we should atleast
# make a good effort here. Modified-since is nice, but we could
# also look into etags, expires, and caching
"""
Registers a static directory handler with Sanic by adding a route to the
router and registering a handler.
:param app: Sanic
:param file_or_directory: File or directory path to serve from
:param uri: URL to serve from
:param pattern: regular expression used to match files in the URL
:param use_modified_since: If true, send file modified time, and return
not modified if the browser's matches the server's
"""
# If we're not trying to match a file directly,
# serve from the folder
if not path.isfile(file_or_directory):
uri += '<file_uri:' + pattern + '>'
async def _handler(request, file_uri=None):
# Using this to determine if the URL is trying to break out of the path
# served. os.path.realpath seems to be very slow
if file_uri and '../' in file_uri:
raise InvalidUsage("Invalid URL")
# Merge served directory and requested file if provided
# Strip all / that in the beginning of the URL to help prevent python
# from herping a derp and treating the uri as an absolute path
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \
if file_uri else file_or_directory
try:
headers = {}
# Check if the client has been sent this file before
# and it has not been modified since
if use_modified_since:
stats = await stat(file_path)
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT',
gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since:
return HTTPResponse(status=304)
headers['Last-Modified'] = modified_since
return await file(file_path, headers=headers)
except:
raise FileNotFound('File not found',
path=file_or_directory,
relative_url=file_uri)
app.route(uri, methods=['GET'])(_handler)

View File

@@ -11,11 +11,13 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
async with aiohttp.ClientSession(cookies=cookies) as session:
async with getattr(session, method)(url, *args, **kwargs) as response:
response.text = await response.text()
response.body = await response.read()
return response
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
*request_args, **request_kwargs):
loop=None, debug=False, *request_args,
**request_kwargs):
results = []
exceptions = []
@@ -33,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
exceptions.append(e)
app.stop()
app.run(host=HOST, port=42101, after_start=_collect_response)
app.run(host=HOST, debug=debug, port=42101,
after_start=_collect_response, loop=loop)
if exceptions:
raise ValueError("Exception during request: {}".format(exceptions))

36
sanic/views.py Normal file
View File

@@ -0,0 +1,36 @@
from .exceptions import InvalidUsage
class HTTPMethodView:
""" Simple class based implementation of view for the sanic.
You should implement methods(get, post, put, patch, delete) for the class
to every HTTP method you want to support.
For example:
class DummyView(View):
def get(self, request, *args, **kwargs):
return text('I am get method')
def put(self, request, *args, **kwargs):
return text('I am put method')
etc.
If someone try use not implemented method, there will be 405 response
If you need any url params just mention them in method definition like:
class DummyView(View):
def get(self, request, my_param_here, *args, **kwargs):
return text('I am get method with %s' % my_param_here)
To add the view into the routing you could use
1) app.add_route(DummyView(), '/')
2) app.route('/')(DummyView())
"""
def __call__(self, request, *args, **kwargs):
handler = getattr(self, request.method.lower(), None)
if handler:
return handler(request, *args, **kwargs)
raise InvalidUsage(
'Method {} not allowed for URL {}'.format(
request.method, request.url), status_code=405)

View File

@@ -1,11 +1,23 @@
"""
Sanic
"""
import codecs
import os
import re
from setuptools import setup
with codecs.open(os.path.join(os.path.abspath(os.path.dirname(
__file__)), 'sanic', '__init__.py'), 'r', 'latin1') as fp:
try:
version = re.findall(r"^__version__ = '([^']+)'\r?$",
fp.read(), re.M)[0]
except IndexError:
raise RuntimeError('Unable to determine version.')
setup(
name='Sanic',
version="0.1.5",
version=version,
url='http://github.com/channelcat/sanic/',
license='MIT',
author='Channel Cat',
@@ -17,6 +29,8 @@ setup(
'uvloop>=0.5.3',
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
'multidict>=2.0',
],
classifiers=[
'Development Status :: 2 - Pre-Alpha',

View File

@@ -15,4 +15,4 @@ async def handle(request):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', handle)
web.run_app(app, port=sys.argv[1])
web.run_app(app, port=sys.argv[1], access_log=None)

View File

@@ -1,3 +1,5 @@
import inspect
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import json, text
@@ -17,7 +19,7 @@ def test_bp():
def handler(request):
return text('Hello')
app.register_blueprint(bp)
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
assert response.text == 'Hello'
@@ -30,7 +32,7 @@ def test_bp_with_url_prefix():
def handler(request):
return text('Hello')
app.register_blueprint(bp)
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'Hello'
@@ -49,8 +51,8 @@ def test_several_bp_with_url_prefix():
def handler2(request):
return text('Hello2')
app.register_blueprint(bp)
app.register_blueprint(bp2)
app.blueprint(bp)
app.blueprint(bp2)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'Hello'
@@ -70,7 +72,7 @@ def test_bp_middleware():
async def handler(request):
return text('FAIL')
app.register_blueprint(blueprint)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app)
@@ -97,7 +99,7 @@ def test_bp_exception_handler():
def handler_exception(request, exception):
return text("OK")
app.register_blueprint(blueprint)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/1')
assert response.status == 400
@@ -140,8 +142,24 @@ def test_bp_listeners():
def handler_6(sanic, loop):
order.append(6)
app.register_blueprint(blueprint)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/')
assert order == [1,2,3,4,5,6]
assert order == [1,2,3,4,5,6]
def test_bp_static():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
blueprint = Blueprint('test_static')
blueprint.static('/testing.file', current_file)
app.blueprint(blueprint)
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents

View File

@@ -86,3 +86,43 @@ def test_middleware_override_response():
assert response.status == 200
assert response.text == 'OK'
def test_middleware_order():
app = Sanic('test_middleware_order')
order = []
@app.middleware('request')
async def request1(request):
order.append(1)
@app.middleware('request')
async def request2(request):
order.append(2)
@app.middleware('request')
async def request3(request):
order.append(3)
@app.middleware('response')
async def response1(request, response):
order.append(6)
@app.middleware('response')
async def response2(request, response):
order.append(5)
@app.middleware('response')
async def response3(request, response):
order.append(4)
@app.route('/')
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app)
assert response.status == 200
assert order == [1,2,3,4,5,6]

View File

@@ -0,0 +1,24 @@
from sanic import Sanic
from sanic.response import json
from sanic.utils import sanic_endpoint_test
from ujson import loads
def test_storage():
app = Sanic('test_text')
@app.middleware('request')
def store(request):
request['user'] = 'sanic'
request['sidekick'] = 'tails'
del request['sidekick']
@app.route('/')
def handler(request):
return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') })
request, response = sanic_endpoint_test(app)
response_json = loads(response.text)
assert response_json['user'] == 'sanic'
assert response_json.get('sidekick') is None

View File

@@ -0,0 +1,40 @@
from sanic import Sanic
import asyncio
from sanic.response import text
from sanic.exceptions import RequestTimeout
from sanic.utils import sanic_endpoint_test
from sanic.config import Config
Config.REQUEST_TIMEOUT = 1
request_timeout_app = Sanic('test_request_timeout')
request_timeout_default_app = Sanic('test_request_timeout_default')
@request_timeout_app.route('/1')
async def handler_1(request):
await asyncio.sleep(1)
return text('OK')
@request_timeout_app.exception(RequestTimeout)
def handler_exception(request, exception):
return text('Request Timeout from error_handler.', 408)
def test_server_error_request_timeout():
request, response = sanic_endpoint_test(request_timeout_app, uri='/1')
assert response.status == 408
assert response.text == 'Request Timeout from error_handler.'
@request_timeout_default_app.route('/1')
async def handler_2(request):
await asyncio.sleep(1)
return text('OK')
def test_default_server_error_request_timeout():
request, response = sanic_endpoint_test(
request_timeout_default_app, uri='/1')
assert response.status == 408
assert response.text == 'Error: Request Timeout'

View File

@@ -56,7 +56,7 @@ def test_query_string():
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")])
request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")])
assert request.args.get('test1') == '1'
assert request.args.get('test2') == 'false'

View File

@@ -84,7 +84,7 @@ def test_dynamic_route_int():
def test_dynamic_route_number():
app = Sanic('test_dynamic_route_int')
app = Sanic('test_dynamic_route_number')
results = []
@@ -105,7 +105,7 @@ def test_dynamic_route_number():
def test_dynamic_route_regex():
app = Sanic('test_dynamic_route_int')
app = Sanic('test_dynamic_route_regex')
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
async def handler(request, folder_id):
@@ -145,7 +145,7 @@ def test_dynamic_route_unhashable():
def test_route_duplicate():
app = Sanic('test_dynamic_route')
app = Sanic('test_route_duplicate')
with pytest.raises(RouteExists):
@app.route('/test')
@@ -164,3 +164,195 @@ def test_route_duplicate():
@app.route('/test/<dynamic>/')
async def handler2(request, dynamic):
pass
def test_method_not_allowed():
app = Sanic('test_method_not_allowed')
@app.route('/test', methods=['GET'])
async def handler(request):
return text('OK')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405
def test_static_add_route():
app = Sanic('test_static_add_route')
async def handler1(request):
return text('OK1')
async def handler2(request):
return text('OK2')
app.add_route(handler1, '/test')
app.add_route(handler2, '/test2')
request, response = sanic_endpoint_test(app, uri='/test')
assert response.text == 'OK1'
request, response = sanic_endpoint_test(app, uri='/test2')
assert response.text == 'OK2'
def test_dynamic_add_route():
app = Sanic('test_dynamic_add_route')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.text == 'OK'
assert results[0] == 'test123'
def test_dynamic_add_route_string():
app = Sanic('test_dynamic_add_route_string')
results = []
async def handler(request, name):
results.append(name)
return text('OK')
app.add_route(handler, '/folder/<name:string>')
request, response = sanic_endpoint_test(app, uri='/folder/test123')
assert response.text == 'OK'
assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
assert response.text == 'OK'
assert results[1] == 'favicon.ico'
def test_dynamic_add_route_int():
app = Sanic('test_dynamic_add_route_int')
results = []
async def handler(request, folder_id):
results.append(folder_id)
return text('OK')
app.add_route(handler, '/folder/<folder_id:int>')
request, response = sanic_endpoint_test(app, uri='/folder/12345')
assert response.text == 'OK'
assert type(results[0]) is int
request, response = sanic_endpoint_test(app, uri='/folder/asdf')
assert response.status == 404
def test_dynamic_add_route_number():
app = Sanic('test_dynamic_add_route_number')
results = []
async def handler(request, weight):
results.append(weight)
return text('OK')
app.add_route(handler, '/weight/<weight:number>')
request, response = sanic_endpoint_test(app, uri='/weight/12345')
assert response.text == 'OK'
assert type(results[0]) is float
request, response = sanic_endpoint_test(app, uri='/weight/1234.56')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
assert response.status == 404
def test_dynamic_add_route_regex():
app = Sanic('test_dynamic_route_int')
async def handler(request, folder_id):
return text('OK')
app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>')
request, response = sanic_endpoint_test(app, uri='/folder/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test1')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
assert response.status == 404
request, response = sanic_endpoint_test(app, uri='/folder/')
assert response.status == 200
def test_dynamic_add_route_unhashable():
app = Sanic('test_dynamic_add_route_unhashable')
async def handler(request, unhashable):
return text('OK')
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
assert response.status == 200
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
assert response.status == 404
def test_add_route_duplicate():
app = Sanic('test_add_route_duplicate')
with pytest.raises(RouteExists):
async def handler1(request):
pass
async def handler2(request):
pass
app.add_route(handler1, '/test')
app.add_route(handler2, '/test')
with pytest.raises(RouteExists):
async def handler1(request, dynamic):
pass
async def handler2(request, dynamic):
pass
app.add_route(handler1, '/test/<dynamic>/')
app.add_route(handler2, '/test/<dynamic>/')
def test_add_route_method_not_allowed():
app = Sanic('test_add_route_method_not_allowed')
async def handler(request):
return text('OK')
app.add_route(handler, '/test', methods=['GET'])
request, response = sanic_endpoint_test(app, uri='/test')
assert response.status == 200
request, response = sanic_endpoint_test(app, method='post', uri='/test')
assert response.status == 405

30
tests/test_static.py Normal file
View File

@@ -0,0 +1,30 @@
import inspect
import os
from sanic import Sanic
from sanic.utils import sanic_endpoint_test
def test_static_file():
current_file = inspect.getfile(inspect.currentframe())
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
app.static('/testing.file', current_file)
request, response = sanic_endpoint_test(app, uri='/testing.file')
assert response.status == 200
assert response.body == current_file_contents
def test_static_directory():
current_file = inspect.getfile(inspect.currentframe())
current_directory = os.path.dirname(os.path.abspath(current_file))
with open(current_file, 'rb') as file:
current_file_contents = file.read()
app = Sanic('test_static')
app.static('/dir', current_directory)
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py')
assert response.status == 200
assert response.body == current_file_contents

155
tests/test_views.py Normal file
View File

@@ -0,0 +1,155 @@
from sanic import Sanic
from sanic.response import text, HTTPResponse
from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint
from sanic.request import Request
from sanic.utils import sanic_endpoint_test
def test_methods():
app = Sanic('test_methods')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
def post(self, request):
return text('I am post method')
def put(self, request):
return text('I am put method')
def patch(self, request):
return text('I am patch method')
def delete(self, request):
return text('I am delete method')
app.add_route(DummyView(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post")
assert response.text == 'I am post method'
request, response = sanic_endpoint_test(app, method="put")
assert response.text == 'I am put method'
request, response = sanic_endpoint_test(app, method="patch")
assert response.text == 'I am patch method'
request, response = sanic_endpoint_test(app, method="delete")
assert response.text == 'I am delete method'
def test_unexisting_methods():
app = Sanic('test_unexisting_methods')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
request, response = sanic_endpoint_test(app, method="get")
assert response.text == 'I am get method'
request, response = sanic_endpoint_test(app, method="post")
assert response.text == 'Error: Method POST not allowed for URL /'
def test_argument_methods():
app = Sanic('test_argument_methods')
class DummyView(HTTPMethodView):
def get(self, request, my_param_here):
return text('I am get method with %s' % my_param_here)
app.add_route(DummyView(), '/<my_param_here>')
request, response = sanic_endpoint_test(app, uri='/test123')
assert response.text == 'I am get method with test123'
def test_with_bp():
app = Sanic('test_with_bp')
bp = Blueprint('test_text')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
bp.add_route(DummyView(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
def test_with_bp_with_url_prefix():
app = Sanic('test_with_bp_with_url_prefix')
bp = Blueprint('test_text', url_prefix='/test1')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
bp.add_route(DummyView(), '/')
app.blueprint(bp)
request, response = sanic_endpoint_test(app, uri='/test1/')
assert response.text == 'I am get method'
def test_with_middleware():
app = Sanic('test_with_middleware')
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
results = []
@app.middleware
async def handler(request):
results.append(request)
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
assert type(results[0]) is Request
def test_with_middleware_response():
app = Sanic('test_with_middleware_response')
results = []
@app.middleware('request')
async def process_response(request):
results.append(request)
@app.middleware('response')
async def process_response(request, response):
results.append(request)
results.append(response)
class DummyView(HTTPMethodView):
def get(self, request):
return text('I am get method')
app.add_route(DummyView(), '/')
request, response = sanic_endpoint_test(app)
assert response.text == 'I am get method'
assert type(results[0]) is Request
assert type(results[1]) is Request
assert issubclass(type(results[2]), HTTPResponse)