├── __init__.py ├── .gitignore ├── sanic_0_1_2 ├── examples │ ├── docker-compose.yml │ ├── Dockerfile │ ├── blueprints.py │ ├── try_protocol.py │ ├── simple_server.py │ └── try_everything.py ├── src │ ├── __init__.py │ ├── log.py │ ├── config.py │ ├── exceptions.py │ ├── utils.py │ ├── response.py │ ├── blueprints.py │ ├── request.py │ ├── router.py │ ├── sanic.py │ └── server.py └── tests │ ├── performance │ ├── bottle │ │ └── simple_server.py │ ├── golang │ │ └── golang.http.go │ ├── sanic │ │ ├── simple_server.py │ │ ├── http_response.py │ │ └── varied_server.py │ ├── aiohttp │ │ └── simple_server.py │ ├── kyoukai │ │ └── simple_server.py │ └── wheezy │ │ └── simple_server.py │ ├── test_exceptions.py │ ├── test_exceptions_handler.py │ ├── test_utf8.py │ ├── test_requests.py │ ├── test_middleware.py │ ├── test_routes.py │ └── test_blueprints.py └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc -------------------------------------------------------------------------------- /sanic_0_1_2/examples/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | sanic: 4 | build: . 5 | ports: 6 | - "8000:8000" -------------------------------------------------------------------------------- /sanic_0_1_2/src/__init__.py: -------------------------------------------------------------------------------- 1 | from .sanic import Sanic 2 | from .blueprints import Blueprint 3 | 4 | __all__ = ['Sanic', 'Blueprint'] 5 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/log.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") 4 | log = logging.getLogger(__name__) 5 | -------------------------------------------------------------------------------- /sanic_0_1_2/examples/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.5 2 | MAINTAINER Channel Cat 3 | 4 | ADD . /code 5 | RUN pip3 install git+https://github.com/channelcat/sanic 6 | 7 | EXPOSE 8000 8 | 9 | WORKDIR /code 10 | 11 | CMD ["python", "simple_server.py"] -------------------------------------------------------------------------------- /sanic_0_1_2/tests/performance/bottle/simple_server.py: -------------------------------------------------------------------------------- 1 | # Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker -b :8000 simple_server:app 2 | import bottle 3 | from bottle import route, run 4 | import ujson 5 | 6 | 7 | @route('/') 8 | def index(): 9 | return ujson.dumps({'test': True}) 10 | 11 | app = bottle.default_app() 12 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/performance/golang/golang.http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "net/http" 7 | ) 8 | 9 | func handler(w http.ResponseWriter, r *http.Request) { 10 | fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) 11 | } 12 | 13 | func main() { 14 | http.HandleFunc("/", handler) 15 | http.ListenAndServe(":" + os.Args[1], nil) 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## sanic_annotation 2 | 3 | > 从最先版本开始阅读、注释,体会功能的增加 4 | 5 | ### [sanic-0.1.2](./sanic_0_1_2) 6 | 7 | 这是Sanic最开始的版本,理论上来说应该也是最易懂且功能最少的一个版本 8 | 9 | 随后将慢慢迭代版本以加深了解Sanic,除了代码中的注释,我还写了一篇关于此版本的总体介绍,结合注解的话,应该可以让你理解起来达到事半功倍的效果 10 | 11 | 见此文 [Sanic源码阅读-基于0.1.2](https://github.com/howie6879/Sanic-For-Pythoneer/blob/master/docs/part2/Sanic%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB%EF%BC%9A%E5%9F%BA%E4%BA%8E0.1.2.md) 12 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/performance/sanic/simple_server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import inspect 4 | 5 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 6 | sys.path.insert(0, currentdir + '/../../../') 7 | 8 | from sanic import Sanic 9 | from sanic.response import json 10 | 11 | app = Sanic("test") 12 | 13 | 14 | @app.route("/") 15 | async def test(request): 16 | return json({"test": True}) 17 | 18 | 19 | app.run(host="0.0.0.0", port=sys.argv[1]) 20 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/performance/aiohttp/simple_server.py: -------------------------------------------------------------------------------- 1 | # Run with python3 simple_server.py PORT 2 | 3 | from aiohttp import web 4 | import asyncio 5 | import sys 6 | import uvloop 7 | import ujson as json 8 | 9 | loop = uvloop.new_event_loop() 10 | asyncio.set_event_loop(loop) 11 | 12 | async def handle(request): 13 | return web.Response(body=json.dumps({"test":True}).encode('utf-8'), content_type='application/json') 14 | 15 | app = web.Application(loop=loop) 16 | app.router.add_route('GET', '/', handle) 17 | 18 | web.run_app(app, port=sys.argv[1]) 19 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/performance/kyoukai/simple_server.py: -------------------------------------------------------------------------------- 1 | # Run with: python3 -O simple_server.py 2 | import asyncio 3 | from kyoukai import Kyoukai, HTTPRequestContext 4 | import logging 5 | import ujson 6 | import uvloop 7 | 8 | loop = uvloop.new_event_loop() 9 | asyncio.set_event_loop(loop) 10 | 11 | kyk = Kyoukai("example_app") 12 | 13 | logger = logging.getLogger("Kyoukai") 14 | logger.setLevel(logging.ERROR) 15 | 16 | @kyk.route("/") 17 | async def index(ctx: HTTPRequestContext): 18 | return ujson.dumps({"test":True}), 200, {"Content-Type": "application/json"} 19 | 20 | kyk.run() -------------------------------------------------------------------------------- /sanic_0_1_2/examples/blueprints.py: -------------------------------------------------------------------------------- 1 | from sanic_0_1_2.src import Sanic 2 | from sanic_0_1_2.src import Blueprint 3 | from sanic_0_1_2.src.response import json, text 4 | 5 | app = Sanic(__name__) 6 | blueprint = Blueprint('name', url_prefix='/my_blueprint') 7 | blueprint2 = Blueprint('name2', url_prefix='/my_blueprint2') 8 | 9 | 10 | @blueprint.route('/foo') 11 | async def foo(request): 12 | return json({'msg': 'hi from blueprint'}) 13 | 14 | 15 | @blueprint2.route('/foo') 16 | async def foo2(request): 17 | return json({'msg': 'hi from blueprint2'}) 18 | 19 | 20 | app.register_blueprint(blueprint) 21 | app.register_blueprint(blueprint2) 22 | 23 | app.run(host="0.0.0.0", port=8000, debug=True) 24 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/config.py: -------------------------------------------------------------------------------- 1 | class Config: 2 | LOGO = """ 3 | ▄▄▄▄▄ 4 | ▀▀▀██████▄▄▄ _______________ 5 | ▄▄▄▄▄ █████████▄ / \\ 6 | ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | 7 | ▀▀█████▄▄ ▀██████▄██ | _________________/ 8 | ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ 9 | ▀▀▀▄ ▀▀███ ▀ ▄▄ 10 | ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ 11 | ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ 12 | ▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ 13 | ▌ ▐▀████▐███▒▒▒▒▒▐██▌ 14 | ▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ 15 | ▀▀█████████▀ 16 | ▄▄██▀██████▀█ 17 | ▄██▀ ▀▀▀ █ 18 | ▄█ ▐▌ 19 | ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ 20 | ▌ ▐ ▀▀▄▄▄▀ 21 | ▀▀▄▄▀ 22 | """ 23 | REQUEST_MAX_SIZE = 100000000 # 100 megababies 24 | REQUEST_TIMEOUT = 60 # 60 seconds 25 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/performance/sanic/http_response.py: -------------------------------------------------------------------------------- 1 | import asyncpg 2 | import sys 3 | import os 4 | import inspect 5 | 6 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 7 | sys.path.insert(0, currentdir + '/../../../') 8 | 9 | import timeit 10 | 11 | from sanic.response import json 12 | 13 | print(json({"test": True}).output()) 14 | 15 | print("Running New 100,000 times") 16 | times = 0 17 | total_time = 0 18 | for n in range(6): 19 | time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000) 20 | print("Took {} seconds".format(time)) 21 | total_time += time 22 | times += 1 23 | print("Average: {}".format(total_time / times)) 24 | 25 | print("Running Old 100,000 times") 26 | times = 0 27 | total_time = 0 28 | for n in range(6): 29 | time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000) 30 | print("Took {} seconds".format(time)) 31 | total_time += time 32 | times += 1 33 | print("Average: {}".format(total_time / times)) 34 | -------------------------------------------------------------------------------- /sanic_0_1_2/examples/try_protocol.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | class EchoServerClientProtocol(asyncio.Protocol): 4 | def connection_made(self, transport): 5 | peername = transport.get_extra_info('peername') 6 | print('Connection from {}'.format(peername)) 7 | self.transport = transport 8 | 9 | def data_received(self, data): 10 | message = data.decode() 11 | print('Data received: {!r}'.format(message)) 12 | 13 | print('Send: {!r}'.format(message)) 14 | self.transport.write(data) 15 | 16 | print('Close the client socket') 17 | self.transport.close() 18 | 19 | loop = asyncio.get_event_loop() 20 | # Each client connection will create a new protocol instance 21 | coro = loop.create_server(EchoServerClientProtocol, '127.0.0.1', 8888) 22 | server = loop.run_until_complete(coro) 23 | 24 | # Serve requests until Ctrl+C is pressed 25 | print('Serving on {}'.format(server.sockets[0].getsockname())) 26 | try: 27 | loop.run_forever() 28 | except KeyboardInterrupt: 29 | pass 30 | 31 | # Close the server 32 | server.close() 33 | loop.run_until_complete(server.wait_closed()) 34 | loop.close() -------------------------------------------------------------------------------- /sanic_0_1_2/examples/simple_server.py: -------------------------------------------------------------------------------- 1 | from sanic_0_1_2.src import Sanic 2 | from sanic_0_1_2.src.response import json 3 | 4 | app = Sanic(__name__) 5 | 6 | 7 | @app.route("/") 8 | async def test(request): 9 | return json({"test": True}) 10 | 11 | 12 | app.run(host="0.0.0.0", port=8000) 13 | 14 | """ 15 | 从最简单的示例出发,一步步地了解Sanic框架的运行机制 16 | 17 | - app.route: 18 | 这是一个装饰器,随着server的启动而启动,可定义参数 uri, methods,目的是为url的path和视图函数对应起来,构建一对映射关系。 19 | 本例中,Sanic.router 类下的 Router.routes = [] 会增加一个名为 Route 的 namedtuple,如下: 20 | [Route(handler=, methods=None, pattern=re.compile('^/$'), parameters=[])] 21 | 22 | - app.run(host="0.0.0.0", port=8000): 23 | Sanic 下的 run 函数,启动一个 http server,主要是启动 run 里面的 serve 函数,参数如下: 24 | - host=host, 25 | - port=port, 26 | - debug=debug, 27 | - after_start=after_start, 28 | - before_stop=before_stop, 29 | - request_handler=self.handle_request, 30 | - request_timeout=self.config.REQUEST_TIMEOUT, 31 | - request_max_size=self.config.REQUEST_MAX_SIZE, 32 | 至此,Sanic 服务启动了 33 | 34 | - server.py: 35 | 这里才是 Sanic 的核心部分,app.run(host="0.0.0.0", port=8000): 里面执行了 server.serve 函数,此时创建了一个 TCP 服务的协程。 36 | 然后通过 loop.run_forever() 运行这个事件循环,以便接收客户端请求以及处理相关事件,每当一个新的客户端建立连接 服务 就会创建一个新的 Protocol 实例。 37 | 接受请求与返回响应离不开其中的 HttpProtocol,里面的函数 支持 接受数据、处理数据、执行视图函数、构建响应数据并返回给客户端 38 | 39 | 具体可看代码里面的注释 40 | 41 | """ 42 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/performance/wheezy/simple_server.py: -------------------------------------------------------------------------------- 1 | # Run with: gunicorn --workers=1 --worker-class=meinheld.gmeinheld.MeinheldWorker simple_server:main 2 | """ Minimal helloworld application. 3 | """ 4 | 5 | from wheezy.http import HTTPResponse 6 | from wheezy.http import WSGIApplication 7 | from wheezy.http.response import json_response 8 | from wheezy.routing import url 9 | from wheezy.web.handlers import BaseHandler 10 | from wheezy.web.middleware import bootstrap_defaults 11 | from wheezy.web.middleware import path_routing_middleware_factory 12 | 13 | import ujson 14 | 15 | class WelcomeHandler(BaseHandler): 16 | 17 | def get(self): 18 | response = HTTPResponse(content_type='application/json; charset=UTF-8') 19 | response.write(ujson.dumps({"test":True})) 20 | return response 21 | 22 | all_urls = [ 23 | url('', WelcomeHandler, name='default'), 24 | # url('', welcome, name='welcome') 25 | ] 26 | 27 | 28 | options = {} 29 | main = WSGIApplication( 30 | middleware=[ 31 | bootstrap_defaults(url_mapping=all_urls), 32 | path_routing_middleware_factory 33 | ], 34 | options=options 35 | ) 36 | 37 | 38 | if __name__ == '__main__': 39 | import sys 40 | from wsgiref.simple_server import make_server 41 | try: 42 | print('Visit http://localhost:{}/'.format(sys.argv[-1])) 43 | make_server('', int(sys.argv[-1]), main).serve_forever() 44 | except KeyboardInterrupt: 45 | pass 46 | print('\nThanks!') 47 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from sanic.response import text 3 | from sanic.exceptions import InvalidUsage, ServerError, NotFound 4 | from sanic.utils import sanic_endpoint_test 5 | 6 | # ------------------------------------------------------------ # 7 | # GET 8 | # ------------------------------------------------------------ # 9 | 10 | exception_app = Sanic('test_exceptions') 11 | 12 | 13 | @exception_app.route('/') 14 | def handler(request): 15 | return text('OK') 16 | 17 | 18 | @exception_app.route('/error') 19 | def handler_error(request): 20 | raise ServerError("OK") 21 | 22 | 23 | @exception_app.route('/404') 24 | def handler_404(request): 25 | raise NotFound("OK") 26 | 27 | 28 | @exception_app.route('/invalid') 29 | def handler_invalid(request): 30 | raise InvalidUsage("OK") 31 | 32 | 33 | def test_no_exception(): 34 | request, response = sanic_endpoint_test(exception_app) 35 | assert response.status == 200 36 | assert response.text == 'OK' 37 | 38 | 39 | def test_server_error_exception(): 40 | request, response = sanic_endpoint_test(exception_app, uri='/error') 41 | assert response.status == 500 42 | 43 | 44 | def test_invalid_usage_exception(): 45 | request, response = sanic_endpoint_test(exception_app, uri='/invalid') 46 | assert response.status == 400 47 | 48 | 49 | def test_not_found_exception(): 50 | request, response = sanic_endpoint_test(exception_app, uri='/404') 51 | assert response.status == 404 52 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/test_exceptions_handler.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from sanic.response import text 3 | from sanic.exceptions import InvalidUsage, ServerError, NotFound 4 | from sanic.utils import sanic_endpoint_test 5 | 6 | exception_handler_app = Sanic('test_exception_handler') 7 | 8 | 9 | @exception_handler_app.route('/1') 10 | def handler_1(request): 11 | raise InvalidUsage("OK") 12 | 13 | 14 | @exception_handler_app.route('/2') 15 | def handler_2(request): 16 | raise ServerError("OK") 17 | 18 | 19 | @exception_handler_app.route('/3') 20 | def handler_3(request): 21 | raise NotFound("OK") 22 | 23 | 24 | @exception_handler_app.exception(NotFound, ServerError) 25 | def handler_exception(request, exception): 26 | return text("OK") 27 | 28 | 29 | def test_invalid_usage_exception_handler(): 30 | request, response = sanic_endpoint_test(exception_handler_app, uri='/1') 31 | assert response.status == 400 32 | 33 | 34 | def test_server_error_exception_handler(): 35 | request, response = sanic_endpoint_test(exception_handler_app, uri='/2') 36 | assert response.status == 200 37 | assert response.text == 'OK' 38 | 39 | 40 | def test_not_found_exception_handler(): 41 | request, response = sanic_endpoint_test(exception_handler_app, uri='/3') 42 | assert response.status == 200 43 | 44 | 45 | def test_text_exception__handler(): 46 | request, response = sanic_endpoint_test( 47 | exception_handler_app, uri='/random') 48 | assert response.status == 200 49 | assert response.text == 'OK' 50 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/exceptions.py: -------------------------------------------------------------------------------- 1 | from .response import text 2 | from traceback import format_exc 3 | 4 | 5 | class SanicException(Exception): 6 | def __init__(self, message, status_code=None): 7 | super().__init__(message) 8 | if status_code is not None: 9 | self.status_code = status_code 10 | 11 | 12 | class NotFound(SanicException): 13 | status_code = 404 14 | 15 | 16 | class InvalidUsage(SanicException): 17 | status_code = 400 18 | 19 | 20 | class ServerError(SanicException): 21 | status_code = 500 22 | 23 | 24 | class Handler: 25 | handlers = None 26 | 27 | def __init__(self, sanic): 28 | self.handlers = {} 29 | self.sanic = sanic 30 | 31 | def add(self, exception, handler): 32 | self.handlers[exception] = handler 33 | 34 | def response(self, request, exception): 35 | """ 36 | Fetches and executes an exception handler and returns a response object 37 | :param request: Request 38 | :param exception: Exception to handle 39 | :return: Response object 40 | """ 41 | handler = self.handlers.get(type(exception), self.default) 42 | response = handler(request=request, exception=exception) 43 | return response 44 | 45 | def default(self, request, exception): 46 | if issubclass(type(exception), SanicException): 47 | return text("Error: {}".format(exception), status=getattr(exception, 'status_code', 500)) 48 | elif self.sanic.debug: 49 | return text("Error: {}\nException: {}".format(exception, format_exc()), status=500) 50 | else: 51 | return text("An error occurred while generating the request", status=500) 52 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/test_utf8.py: -------------------------------------------------------------------------------- 1 | from json import loads as json_loads, dumps as json_dumps 2 | from sanic import Sanic 3 | from sanic.response import json, text 4 | from sanic.utils import sanic_endpoint_test 5 | 6 | 7 | # ------------------------------------------------------------ # 8 | # UTF-8 9 | # ------------------------------------------------------------ # 10 | 11 | def test_utf8_query_string(): 12 | app = Sanic('test_utf8_query_string') 13 | 14 | @app.route('/') 15 | async def handler(request): 16 | return text('OK') 17 | 18 | request, response = sanic_endpoint_test(app, params=[("utf8", '✓')]) 19 | assert request.args.get('utf8') == '✓' 20 | 21 | 22 | def test_utf8_response(): 23 | app = Sanic('test_utf8_response') 24 | 25 | @app.route('/') 26 | async def handler(request): 27 | return text('✓') 28 | 29 | request, response = sanic_endpoint_test(app) 30 | assert response.text == '✓' 31 | 32 | 33 | def skip_test_utf8_route(): 34 | app = Sanic('skip_test_utf8_route') 35 | 36 | @app.route('/') 37 | async def handler(request): 38 | return text('OK') 39 | 40 | # UTF-8 Paths are not supported 41 | request, response = sanic_endpoint_test(app, route='/✓', uri='/✓') 42 | assert response.text == 'OK' 43 | 44 | 45 | def test_utf8_post_json(): 46 | app = Sanic('test_utf8_post_json') 47 | 48 | @app.route('/') 49 | async def handler(request): 50 | return text('OK') 51 | 52 | payload = {'test': '✓'} 53 | headers = {'content-type': 'application/json'} 54 | 55 | request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers) 56 | 57 | assert request.json.get('test') == '✓' 58 | assert response.text == 'OK' 59 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/utils.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | from sanic.log import log 3 | 4 | HOST = '127.0.0.1' 5 | PORT = 42101 6 | 7 | 8 | async def local_request(method, uri, *args, **kwargs): 9 | url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri) 10 | log.info(url) 11 | async with aiohttp.ClientSession() as session: 12 | async with getattr(session, method)(url, *args, **kwargs) as response: 13 | response.text = await response.text() 14 | return response 15 | 16 | 17 | def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, 18 | *request_args, **request_kwargs): 19 | results = [] 20 | exceptions = [] 21 | 22 | if gather_request: 23 | @app.middleware 24 | def _collect_request(request): 25 | results.append(request) 26 | 27 | async def _collect_response(loop): 28 | try: 29 | response = await local_request(method, uri, *request_args, 30 | **request_kwargs) 31 | results.append(response) 32 | except Exception as e: 33 | exceptions.append(e) 34 | app.stop() 35 | 36 | app.run(host=HOST, port=42101, after_start=_collect_response) 37 | 38 | if exceptions: 39 | raise ValueError("Exception during request: {}".format(exceptions)) 40 | 41 | if gather_request: 42 | try: 43 | request, response = results 44 | return request, response 45 | except: 46 | raise ValueError( 47 | "request and response object expected, got ({})".format( 48 | results)) 49 | else: 50 | try: 51 | return results[0] 52 | except: 53 | raise ValueError( 54 | "request object expected, got ({})".format(results)) 55 | -------------------------------------------------------------------------------- /sanic_0_1_2/examples/try_everything.py: -------------------------------------------------------------------------------- 1 | from sanic_0_1_2.src import Sanic 2 | from sanic_0_1_2.src.log import log 3 | from sanic_0_1_2.src.response import json, text 4 | from sanic_0_1_2.src.exceptions import ServerError 5 | 6 | app = Sanic(__name__) 7 | 8 | 9 | @app.route("/") 10 | async def test_async(request): 11 | return json({"test": True}) 12 | 13 | 14 | @app.route("/sync", methods=['GET', 'POST']) 15 | def test_sync(request): 16 | return json({"test": True}) 17 | 18 | 19 | @app.route("/dynamic//") 20 | def test_params(request, name, id): 21 | return text("yeehaww {} {}".format(name, id)) 22 | 23 | 24 | @app.route("/exception") 25 | def exception(request): 26 | raise ServerError("It's dead jim") 27 | 28 | 29 | @app.route("/await") 30 | async def test_await(request): 31 | import asyncio 32 | await asyncio.sleep(5) 33 | return text("I'm feeling sleepy") 34 | 35 | 36 | # ----------------------------------------------- # 37 | # Exceptions 38 | # ----------------------------------------------- # 39 | 40 | @app.exception(ServerError) 41 | async def test(request, exception): 42 | return json({"exception": "{}".format(exception), "status": exception.status_code}, status=exception.status_code) 43 | 44 | 45 | # ----------------------------------------------- # 46 | # Read from request 47 | # ----------------------------------------------- # 48 | 49 | @app.route("/json") 50 | def post_json(request): 51 | return json({"received": True, "message": request.json}) 52 | 53 | 54 | @app.route("/form") 55 | def post_json(request): 56 | return json({"received": True, "form_data": request.form, "test": request.form.get('test')}) 57 | 58 | 59 | @app.route("/query_string") 60 | def query_string(request): 61 | return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string}) 62 | 63 | 64 | # ----------------------------------------------- # 65 | # Run Server 66 | # ----------------------------------------------- # 67 | 68 | def after_start(loop): 69 | log.info("OH OH OH OH OHHHHHHHH") 70 | 71 | 72 | def before_stop(loop): 73 | log.info("TRIED EVERYTHING") 74 | 75 | 76 | app.run(host="0.0.0.0", port=8000, debug=True, after_start=after_start, before_stop=before_stop) 77 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/test_requests.py: -------------------------------------------------------------------------------- 1 | from json import loads as json_loads, dumps as json_dumps 2 | from sanic import Sanic 3 | from sanic.response import json, text 4 | from sanic.utils import sanic_endpoint_test 5 | 6 | 7 | # ------------------------------------------------------------ # 8 | # GET 9 | # ------------------------------------------------------------ # 10 | 11 | def test_sync(): 12 | app = Sanic('test_text') 13 | 14 | @app.route('/') 15 | def handler(request): 16 | return text('Hello') 17 | 18 | request, response = sanic_endpoint_test(app) 19 | 20 | assert response.text == 'Hello' 21 | 22 | 23 | def test_text(): 24 | app = Sanic('test_text') 25 | 26 | @app.route('/') 27 | async def handler(request): 28 | return text('Hello') 29 | 30 | request, response = sanic_endpoint_test(app) 31 | 32 | assert response.text == 'Hello' 33 | 34 | 35 | def test_json(): 36 | app = Sanic('test_json') 37 | 38 | @app.route('/') 39 | async def handler(request): 40 | return json({"test": True}) 41 | 42 | request, response = sanic_endpoint_test(app) 43 | 44 | try: 45 | results = json_loads(response.text) 46 | except: 47 | raise ValueError("Expected JSON response but got '{}'".format(response)) 48 | 49 | assert results.get('test') == True 50 | 51 | 52 | def test_query_string(): 53 | app = Sanic('test_query_string') 54 | 55 | @app.route('/') 56 | async def handler(request): 57 | return text('OK') 58 | 59 | request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")]) 60 | 61 | assert request.args.get('test1') == '1' 62 | assert request.args.get('test2') == 'false' 63 | 64 | 65 | # ------------------------------------------------------------ # 66 | # POST 67 | # ------------------------------------------------------------ # 68 | 69 | def test_post_json(): 70 | app = Sanic('test_post_json') 71 | 72 | @app.route('/') 73 | async def handler(request): 74 | return text('OK') 75 | 76 | payload = {'test': 'OK'} 77 | headers = {'content-type': 'application/json'} 78 | 79 | request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers) 80 | 81 | assert request.json.get('test') == 'OK' 82 | assert response.text == 'OK' 83 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/response.py: -------------------------------------------------------------------------------- 1 | import ujson 2 | 3 | STATUS_CODES = { 4 | 200: b'OK', 5 | 400: b'Bad Request', 6 | 401: b'Unauthorized', 7 | 402: b'Payment Required', 8 | 403: b'Forbidden', 9 | 404: b'Not Found', 10 | 405: b'Method Not Allowed', 11 | 500: b'Internal Server Error', 12 | 501: b'Not Implemented', 13 | 502: b'Bad Gateway', 14 | 503: b'Service Unavailable', 15 | 504: b'Gateway Timeout', 16 | } 17 | 18 | 19 | class HTTPResponse: 20 | __slots__ = ('body', 'status', 'content_type', 'headers') 21 | 22 | def __init__(self, body=None, status=200, headers=None, content_type='text/plain', body_bytes=b''): 23 | self.content_type = content_type 24 | 25 | if body is not None: 26 | self.body = body.encode('utf-8') 27 | else: 28 | self.body = body_bytes 29 | 30 | self.status = status 31 | self.headers = headers or {} 32 | 33 | def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): 34 | # This is all returned in a kind-of funky way 35 | # We tried to make this as fast as possible in pure python 36 | timeout_header = b'' 37 | if keep_alive and keep_alive_timeout: 38 | timeout_header = b'Keep-Alive: timeout=%d\r\n' % keep_alive_timeout 39 | 40 | headers = b'' 41 | if self.headers: 42 | headers = b''.join( 43 | b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) 44 | for name, value in self.headers.items() 45 | ) 46 | return b'HTTP/%b %d %b\r\nContent-Type: %b\r\nContent-Length: %d\r\nConnection: %b\r\n%b%b\r\n%b' % ( 47 | version.encode(), 48 | self.status, 49 | STATUS_CODES.get(self.status, b'FAIL'), 50 | self.content_type.encode(), 51 | len(self.body), 52 | b'keep-alive' if keep_alive else b'close', 53 | timeout_header, 54 | headers, 55 | self.body 56 | ) 57 | 58 | 59 | def json(body, status=200, headers=None): 60 | return HTTPResponse(ujson.dumps(body), headers=headers, status=status, 61 | content_type="application/json; charset=utf-8") 62 | 63 | 64 | def text(body, status=200, headers=None): 65 | return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8") 66 | 67 | 68 | def html(body, status=200, headers=None): 69 | return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8") 70 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/test_middleware.py: -------------------------------------------------------------------------------- 1 | from json import loads as json_loads, dumps as json_dumps 2 | from sanic import Sanic 3 | from sanic.request import Request 4 | from sanic.response import json, text, HTTPResponse 5 | from sanic.utils import sanic_endpoint_test 6 | 7 | 8 | # ------------------------------------------------------------ # 9 | # GET 10 | # ------------------------------------------------------------ # 11 | 12 | def test_middleware_request(): 13 | app = Sanic('test_middleware_request') 14 | 15 | results = [] 16 | 17 | @app.middleware 18 | async def handler(request): 19 | results.append(request) 20 | 21 | @app.route('/') 22 | async def handler(request): 23 | return text('OK') 24 | 25 | request, response = sanic_endpoint_test(app) 26 | 27 | assert response.text == 'OK' 28 | assert type(results[0]) is Request 29 | 30 | 31 | def test_middleware_response(): 32 | app = Sanic('test_middleware_response') 33 | 34 | results = [] 35 | 36 | @app.middleware('request') 37 | async def process_response(request): 38 | results.append(request) 39 | 40 | @app.middleware('response') 41 | async def process_response(request, response): 42 | results.append(request) 43 | results.append(response) 44 | 45 | @app.route('/') 46 | async def handler(request): 47 | return text('OK') 48 | 49 | request, response = sanic_endpoint_test(app) 50 | 51 | assert response.text == 'OK' 52 | assert type(results[0]) is Request 53 | assert type(results[1]) is Request 54 | assert issubclass(type(results[2]), HTTPResponse) 55 | 56 | 57 | def test_middleware_override_request(): 58 | app = Sanic('test_middleware_override_request') 59 | 60 | @app.middleware 61 | async def halt_request(request): 62 | return text('OK') 63 | 64 | @app.route('/') 65 | async def handler(request): 66 | return text('FAIL') 67 | 68 | response = sanic_endpoint_test(app, gather_request=False) 69 | 70 | assert response.status == 200 71 | assert response.text == 'OK' 72 | 73 | 74 | def test_middleware_override_response(): 75 | app = Sanic('test_middleware_override_response') 76 | 77 | @app.middleware('response') 78 | async def process_response(request, response): 79 | return text('OK') 80 | 81 | @app.route('/') 82 | async def handler(request): 83 | return text('FAIL') 84 | 85 | request, response = sanic_endpoint_test(app) 86 | 87 | assert response.status == 200 88 | assert response.text == 'OK' 89 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/performance/sanic/varied_server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import inspect 4 | 5 | currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 6 | sys.path.insert(0, currentdir + '/../../../') 7 | 8 | from sanic import Sanic 9 | from sanic.response import json, text 10 | from sanic.exceptions import ServerError 11 | 12 | app = Sanic("test") 13 | 14 | 15 | @app.route("/") 16 | async def test(request): 17 | return json({"test": True}) 18 | 19 | 20 | @app.route("/sync", methods=['GET', 'POST']) 21 | def test(request): 22 | return json({"test": True}) 23 | 24 | 25 | @app.route("/text//") 26 | def rtext(request, name, butt): 27 | return text("yeehaww {} {}".format(name, butt)) 28 | 29 | 30 | @app.route("/exception") 31 | def exception(request): 32 | raise ServerError("yep") 33 | 34 | 35 | @app.route("/exception/async") 36 | async def test(request): 37 | raise ServerError("asunk") 38 | 39 | 40 | @app.route("/post_json") 41 | def post_json(request): 42 | return json({"received": True, "message": request.json}) 43 | 44 | 45 | @app.route("/query_string") 46 | def query_string(request): 47 | return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string}) 48 | 49 | 50 | import sys 51 | 52 | app.run(host="0.0.0.0", port=sys.argv[1]) 53 | 54 | 55 | 56 | # import asyncio_redis 57 | # import asyncpg 58 | # async def setup(sanic, loop): 59 | # sanic.conn = [] 60 | # sanic.redis = [] 61 | # for x in range(10): 62 | # sanic.conn.append(await asyncpg.connect(user='postgres', password='zomgdev', database='postgres', host='192.168.99.100')) 63 | # for n in range(30): 64 | # connection = await asyncio_redis.Connection.create(host='192.168.99.100', port=6379) 65 | # sanic.redis.append(connection) 66 | 67 | 68 | # c=0 69 | # @app.route("/postgres") 70 | # async def postgres(request): 71 | # global c 72 | # values = await app.conn[c].fetch('''SELECT * FROM players''') 73 | # c += 1 74 | # if c == 10: 75 | # c = 0 76 | # return text("yep") 77 | 78 | # r=0 79 | # @app.route("/redis") 80 | # async def redis(request): 81 | # global r 82 | # try: 83 | # values = await app.redis[r].get('my_key') 84 | # except asyncio_redis.exceptions.ConnectionLostError: 85 | # app.redis[r] = await asyncio_redis.Connection.create(host='127.0.0.1', port=6379) 86 | # values = await app.redis[r].get('my_key') 87 | 88 | # r += 1 89 | # if r == 30: 90 | # r = 0 91 | # return text(values) 92 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/test_routes.py: -------------------------------------------------------------------------------- 1 | from json import loads as json_loads, dumps as json_dumps 2 | from sanic import Sanic 3 | from sanic.response import json, text 4 | from sanic.utils import sanic_endpoint_test 5 | 6 | 7 | # ------------------------------------------------------------ # 8 | # UTF-8 9 | # ------------------------------------------------------------ # 10 | 11 | def test_dynamic_route(): 12 | app = Sanic('test_dynamic_route') 13 | 14 | results = [] 15 | 16 | @app.route('/folder/') 17 | async def handler(request, name): 18 | results.append(name) 19 | return text('OK') 20 | 21 | request, response = sanic_endpoint_test(app, uri='/folder/test123') 22 | 23 | assert response.text == 'OK' 24 | assert results[0] == 'test123' 25 | 26 | 27 | def test_dynamic_route_string(): 28 | app = Sanic('test_dynamic_route_string') 29 | 30 | results = [] 31 | 32 | @app.route('/folder/') 33 | async def handler(request, name): 34 | results.append(name) 35 | return text('OK') 36 | 37 | request, response = sanic_endpoint_test(app, uri='/folder/test123') 38 | 39 | assert response.text == 'OK' 40 | assert results[0] == 'test123' 41 | 42 | 43 | def test_dynamic_route_int(): 44 | app = Sanic('test_dynamic_route_int') 45 | 46 | results = [] 47 | 48 | @app.route('/folder/') 49 | async def handler(request, folder_id): 50 | results.append(folder_id) 51 | return text('OK') 52 | 53 | request, response = sanic_endpoint_test(app, uri='/folder/12345') 54 | assert response.text == 'OK' 55 | assert type(results[0]) is int 56 | 57 | request, response = sanic_endpoint_test(app, uri='/folder/asdf') 58 | assert response.status == 404 59 | 60 | 61 | def test_dynamic_route_number(): 62 | app = Sanic('test_dynamic_route_int') 63 | 64 | results = [] 65 | 66 | @app.route('/weight/') 67 | async def handler(request, weight): 68 | results.append(weight) 69 | return text('OK') 70 | 71 | request, response = sanic_endpoint_test(app, uri='/weight/12345') 72 | assert response.text == 'OK' 73 | assert type(results[0]) is float 74 | 75 | request, response = sanic_endpoint_test(app, uri='/weight/1234.56') 76 | assert response.status == 200 77 | 78 | request, response = sanic_endpoint_test(app, uri='/weight/1234-56') 79 | assert response.status == 404 80 | 81 | 82 | def test_dynamic_route_regex(): 83 | app = Sanic('test_dynamic_route_int') 84 | 85 | @app.route('/folder/') 86 | async def handler(request, folder_id): 87 | return text('OK') 88 | 89 | request, response = sanic_endpoint_test(app, uri='/folder/test') 90 | assert response.status == 200 91 | 92 | request, response = sanic_endpoint_test(app, uri='/folder/test1') 93 | assert response.status == 404 94 | 95 | request, response = sanic_endpoint_test(app, uri='/folder/test-123') 96 | assert response.status == 404 97 | 98 | request, response = sanic_endpoint_test(app, uri='/folder/') 99 | assert response.status == 200 100 | -------------------------------------------------------------------------------- /sanic_0_1_2/tests/test_blueprints.py: -------------------------------------------------------------------------------- 1 | from sanic import Sanic 2 | from sanic.blueprints import Blueprint 3 | from sanic.response import json, text 4 | from sanic.utils import sanic_endpoint_test 5 | from sanic.exceptions import NotFound, ServerError, InvalidUsage 6 | 7 | 8 | # ------------------------------------------------------------ # 9 | # GET 10 | # ------------------------------------------------------------ # 11 | 12 | def test_bp(): 13 | app = Sanic('test_text') 14 | bp = Blueprint('test_text') 15 | 16 | @bp.route('/') 17 | def handler(request): 18 | return text('Hello') 19 | 20 | app.register_blueprint(bp) 21 | request, response = sanic_endpoint_test(app) 22 | 23 | assert response.text == 'Hello' 24 | 25 | def test_bp_with_url_prefix(): 26 | app = Sanic('test_text') 27 | bp = Blueprint('test_text', url_prefix='/test1') 28 | 29 | @bp.route('/') 30 | def handler(request): 31 | return text('Hello') 32 | 33 | app.register_blueprint(bp) 34 | request, response = sanic_endpoint_test(app, uri='/test1/') 35 | 36 | assert response.text == 'Hello' 37 | 38 | 39 | def test_several_bp_with_url_prefix(): 40 | app = Sanic('test_text') 41 | bp = Blueprint('test_text', url_prefix='/test1') 42 | bp2 = Blueprint('test_text2', url_prefix='/test2') 43 | 44 | @bp.route('/') 45 | def handler(request): 46 | return text('Hello') 47 | 48 | @bp2.route('/') 49 | def handler2(request): 50 | return text('Hello2') 51 | 52 | app.register_blueprint(bp) 53 | app.register_blueprint(bp2) 54 | request, response = sanic_endpoint_test(app, uri='/test1/') 55 | assert response.text == 'Hello' 56 | 57 | request, response = sanic_endpoint_test(app, uri='/test2/') 58 | assert response.text == 'Hello2' 59 | 60 | 61 | def test_bp_middleware(): 62 | app = Sanic('test_middleware') 63 | blueprint = Blueprint('test_middleware') 64 | 65 | @blueprint.middleware('response') 66 | async def process_response(request, response): 67 | return text('OK') 68 | 69 | @app.route('/') 70 | async def handler(request): 71 | return text('FAIL') 72 | 73 | app.register_blueprint(blueprint) 74 | 75 | request, response = sanic_endpoint_test(app) 76 | 77 | assert response.status == 200 78 | assert response.text == 'OK' 79 | 80 | def test_bp_exception_handler(): 81 | app = Sanic('test_middleware') 82 | blueprint = Blueprint('test_middleware') 83 | 84 | @blueprint.route('/1') 85 | def handler_1(request): 86 | raise InvalidUsage("OK") 87 | 88 | @blueprint.route('/2') 89 | def handler_2(request): 90 | raise ServerError("OK") 91 | 92 | @blueprint.route('/3') 93 | def handler_3(request): 94 | raise NotFound("OK") 95 | 96 | @blueprint.exception(NotFound, ServerError) 97 | def handler_exception(request, exception): 98 | return text("OK") 99 | 100 | app.register_blueprint(blueprint) 101 | 102 | request, response = sanic_endpoint_test(app, uri='/1') 103 | assert response.status == 400 104 | 105 | 106 | request, response = sanic_endpoint_test(app, uri='/2') 107 | assert response.status == 200 108 | assert response.text == 'OK' 109 | 110 | request, response = sanic_endpoint_test(app, uri='/3') 111 | assert response.status == 200 -------------------------------------------------------------------------------- /sanic_0_1_2/src/blueprints.py: -------------------------------------------------------------------------------- 1 | class BlueprintSetup: 2 | """ 3 | 这个类的主要作用是: 4 | - 添加路由 5 | - 添加exception 6 | - 添加中间件 7 | 会在Blueprint.make_setup_state中被实例化,以路由为例,会调用Router.add从而添加一个uri与视图函数的映射关系 8 | """ 9 | 10 | def __init__(self, blueprint, app, options): 11 | self.app = app 12 | self.blueprint = blueprint 13 | self.options = options 14 | 15 | url_prefix = self.options.get('url_prefix') 16 | if url_prefix is None: 17 | url_prefix = self.blueprint.url_prefix 18 | 19 | #: The prefix that should be used for all URLs defined on the 20 | #: blueprint. 21 | self.url_prefix = url_prefix 22 | 23 | def add_route(self, handler, uri, methods): 24 | """ 25 | A helper method to register a handler to the application url routes. 26 | """ 27 | if self.url_prefix: 28 | uri = self.url_prefix + uri 29 | 30 | self.app.router.add(uri, methods, handler) 31 | 32 | def add_exception(self, handler, *args, **kwargs): 33 | """ 34 | Registers exceptions to sanic 35 | """ 36 | self.app.exception(*args, **kwargs)(handler) 37 | 38 | def add_middleware(self, middleware, *args, **kwargs): 39 | """ 40 | Registers middleware to sanic 41 | """ 42 | if args or kwargs: 43 | self.app.middleware(*args, **kwargs)(middleware) 44 | else: 45 | self.app.middleware(middleware) 46 | 47 | 48 | class Blueprint: 49 | def __init__(self, name, url_prefix=None): 50 | """ 51 | 蓝图类 52 | :param name: 蓝图名称 53 | :param url_prefix: 该蓝图的url前缀 54 | """ 55 | self.name = name 56 | self.url_prefix = url_prefix 57 | self.deferred_functions = [] 58 | 59 | def record(self, func): 60 | """ 61 | Registers a callback function that is invoked when the blueprint is 62 | registered on the application. 63 | """ 64 | self.deferred_functions.append(func) 65 | 66 | def make_setup_state(self, app, options): 67 | """ 68 | """ 69 | return BlueprintSetup(self, app, options) 70 | 71 | def register(self, app, options): 72 | """ 73 | """ 74 | state = self.make_setup_state(app, options) 75 | for deferred in self.deferred_functions: 76 | deferred(state) 77 | 78 | def route(self, uri, methods=None): 79 | """ 80 | 蓝图路由装饰器 81 | """ 82 | def decorator(handler): 83 | self.record(lambda s: s.add_route(handler, uri, methods)) 84 | return handler 85 | return decorator 86 | 87 | def middleware(self, *args, **kwargs): 88 | """ 89 | """ 90 | 91 | def register_middleware(middleware): 92 | self.record(lambda s: s.add_middleware(middleware, *args, **kwargs)) 93 | return middleware 94 | 95 | # Detect which way this was called, @middleware or @middleware('AT') 96 | if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): 97 | args = [] 98 | return register_middleware(args[0]) 99 | else: 100 | return register_middleware 101 | 102 | def exception(self, *args, **kwargs): 103 | """ 104 | """ 105 | def decorator(handler): 106 | self.record(lambda s: s.add_exception(handler, *args, **kwargs)) 107 | return handler 108 | return decorator 109 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/request.py: -------------------------------------------------------------------------------- 1 | from cgi import parse_header 2 | from collections import namedtuple 3 | from httptools import parse_url 4 | from urllib.parse import parse_qs 5 | from ujson import loads as json_loads 6 | 7 | from .log import log 8 | 9 | 10 | class RequestParameters(dict): 11 | """ 12 | Hosts a dict with lists as values where get returns the first 13 | value of the list and getlist returns the whole shebang 14 | """ 15 | 16 | def __init__(self, *args, **kwargs): 17 | self.super = super() 18 | self.super.__init__(*args, **kwargs) 19 | 20 | def get(self, name, default=None): 21 | values = self.super.get(name) 22 | return values[0] if values else default 23 | 24 | def getlist(self, name, default=None): 25 | return self.super.get(name, default) 26 | 27 | 28 | class Request: 29 | __slots__ = ( 30 | 'url', 'headers', 'version', 'method', 31 | 'query_string', 'body', 32 | 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 33 | ) 34 | 35 | def __init__(self, url_bytes, headers, version, method): 36 | # TODO: Content-Encoding detection 37 | url_parsed = parse_url(url_bytes) 38 | self.url = url_parsed.path.decode('utf-8') 39 | self.headers = headers 40 | self.version = version 41 | self.method = method 42 | self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None 43 | 44 | # Init but do not inhale 45 | self.body = None 46 | self.parsed_json = None 47 | self.parsed_form = None 48 | self.parsed_files = None 49 | self.parsed_args = None 50 | 51 | @property 52 | def json(self): 53 | if not self.parsed_json: 54 | try: 55 | self.parsed_json = json_loads(self.body) 56 | except: 57 | pass 58 | 59 | return self.parsed_json 60 | 61 | @property 62 | def form(self): 63 | if self.parsed_form is None: 64 | self.parsed_form = {} 65 | self.parsed_files = {} 66 | content_type, parameters = parse_header(self.headers.get('Content-Type')) 67 | try: 68 | if content_type is None or content_type == 'application/x-www-form-urlencoded': 69 | self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8'))) 70 | elif content_type == 'multipart/form-data': 71 | # TODO: Stream this instead of reading to/from memory 72 | boundary = parameters['boundary'].encode('utf-8') 73 | self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary) 74 | except Exception as e: 75 | log.exception(e) 76 | pass 77 | 78 | return self.parsed_form 79 | 80 | @property 81 | def files(self): 82 | if self.parsed_files is None: 83 | self.form # compute form to get files 84 | 85 | return self.parsed_files 86 | 87 | @property 88 | def args(self): 89 | if self.parsed_args is None: 90 | if self.query_string: 91 | self.parsed_args = RequestParameters(parse_qs(self.query_string)) 92 | else: 93 | self.parsed_args = {} 94 | 95 | return self.parsed_args 96 | 97 | 98 | File = namedtuple('File', ['type', 'body', 'name']) 99 | 100 | 101 | def parse_multipart_form(body, boundary): 102 | """ 103 | Parses a request body and returns fields and files 104 | :param body: Bytes request body 105 | :param boundary: Bytes multipart boundary 106 | :return: fields (dict), files (dict) 107 | """ 108 | files = {} 109 | fields = {} 110 | 111 | form_parts = body.split(boundary) 112 | for form_part in form_parts[1:-1]: 113 | file_name = None 114 | file_type = None 115 | field_name = None 116 | line_index = 2 117 | line_end_index = 0 118 | while not line_end_index == -1: 119 | line_end_index = form_part.find(b'\r\n', line_index) 120 | form_line = form_part[line_index:line_end_index].decode('utf-8') 121 | line_index = line_end_index + 2 122 | 123 | if not form_line: 124 | break 125 | 126 | colon_index = form_line.index(':') 127 | form_header_field = form_line[0:colon_index] 128 | form_header_value, form_parameters = parse_header(form_line[colon_index + 2:]) 129 | 130 | if form_header_field == 'Content-Disposition': 131 | if 'filename' in form_parameters: 132 | file_name = form_parameters['filename'] 133 | field_name = form_parameters.get('name') 134 | elif form_header_field == 'Content-Type': 135 | file_type = form_header_value 136 | 137 | post_data = form_part[line_index:-4] 138 | if file_name or file_type: 139 | files[field_name] = File(type=file_type, name=file_name, body=post_data) 140 | else: 141 | fields[field_name] = post_data.decode('utf-8') 142 | 143 | return fields, files 144 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/router.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import namedtuple 3 | from .exceptions import NotFound, InvalidUsage 4 | 5 | Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) 6 | Parameter = namedtuple("Parameter", ['name', 'cast']) 7 | 8 | 9 | class Router: 10 | """ 11 | Router supports basic routing with parameters and method checks 12 | Usage: 13 | @sanic.route('/my/url/', methods=['GET', 'POST', ...]) 14 | def my_route(request, my_parameter): 15 | do stuff... 16 | 17 | Parameters will be passed as keyword arguments to the request handling function provided 18 | Parameters can also have a type by appending :type to the . If no type is provided, 19 | a string is expected. A regular expression can also be passed in as the type 20 | 21 | TODO: 22 | This probably needs optimization for larger sets of routes, 23 | since it checks every route until it finds a match which is bad and I should feel bad 24 | """ 25 | routes = None 26 | regex_types = { 27 | "string": (None, "\w+"), 28 | "int": (int, "\d+"), 29 | "number": (float, "[0-9\\.]+"), 30 | "alpha": (None, "[A-Za-z]+"), 31 | } 32 | 33 | def __init__(self): 34 | self.routes = [] 35 | 36 | def add(self, uri, methods, handler): 37 | """ 38 | Adds a handler to the route list 39 | :param uri: Path to match 40 | :param methods: Array of accepted method names. If none are provided, any method is allowed 41 | :param handler: Request handler function. When executed, it should provide a response object. 42 | :return: Nothing 43 | """ 44 | 45 | # Dict for faster lookups of if method allowed 46 | methods_dict = {method: True for method in methods} if methods else None 47 | 48 | parameters = [] 49 | 50 | def add_parameter(match): 51 | # We could receive NAME or NAME:PATTERN 52 | parts = match.group(1).split(':') 53 | if len(parts) == 2: 54 | parameter_name, parameter_pattern = parts 55 | else: 56 | parameter_name = parts[0] 57 | parameter_pattern = 'string' 58 | 59 | # Pull from pre-configured types 60 | parameter_regex = self.regex_types.get(parameter_pattern) 61 | if parameter_regex: 62 | parameter_type, parameter_pattern = parameter_regex 63 | else: 64 | parameter_type = None 65 | 66 | parameter = Parameter(name=parameter_name, cast=parameter_type) 67 | parameters.append(parameter) 68 | 69 | return "({})".format(parameter_pattern) 70 | 71 | pattern_string = re.sub("<(.+?)>", add_parameter, uri) 72 | pattern = re.compile("^{}$".format(pattern_string)) 73 | 74 | route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters) 75 | self.routes.append(route) 76 | 77 | def get(self, request): 78 | """ 79 | Gets a request handler based on the URL of the request, or raises an error 80 | :param request: Request object 81 | :return: handler, arguments, keyword arguments 82 | """ 83 | 84 | route = None 85 | args = [] 86 | kwargs = {} 87 | for _route in self.routes: 88 | match = _route.pattern.match(request.url) 89 | if match: 90 | for index, parameter in enumerate(_route.parameters, start=1): 91 | value = match.group(index) 92 | kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value 93 | route = _route 94 | break 95 | 96 | if route: 97 | if route.methods and request.method not in route.methods: 98 | raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), 99 | status_code=405) 100 | return route.handler, args, kwargs 101 | else: 102 | raise NotFound("Requested URL {} not found".format(request.url)) 103 | 104 | 105 | class SimpleRouter: 106 | """ 107 | Simple router records and reads all routes from a dictionary 108 | It does not support parameters in routes, but is very fast 109 | """ 110 | routes = None 111 | 112 | def __init__(self): 113 | self.routes = {} 114 | 115 | def add(self, uri, methods, handler): 116 | # Dict for faster lookups of method allowed 117 | methods_dict = {method: True for method in methods} if methods else None 118 | self.routes[uri] = Route(handler=handler, methods=methods_dict, pattern=uri, parameters=None) 119 | 120 | def get(self, request): 121 | route = self.routes.get(request.url) 122 | if route: 123 | if route.methods and request.method not in route.methods: 124 | raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), 125 | status_code=405) 126 | return route.handler, [], {} 127 | else: 128 | raise NotFound("Requested URL {} not found".format(request.url)) 129 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/sanic.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from inspect import isawaitable 3 | from traceback import format_exc 4 | 5 | from .config import Config 6 | from .exceptions import Handler 7 | from .log import log, logging 8 | from .response import HTTPResponse 9 | from .router import Router 10 | from .server import serve 11 | from .exceptions import ServerError 12 | 13 | 14 | class Sanic: 15 | def __init__(self, name, router=None, error_handler=None): 16 | """ 17 | 构建一个 Sanic 服务必须要实例化的类 18 | :param name: 服务名 默认为 __name__ 19 | :param router: 路由类 默认为 Router 20 | :param error_handler: 错误处理类 默认 Handler 21 | """ 22 | self.name = name 23 | self.router = router or Router() 24 | self.error_handler = error_handler or Handler(self) 25 | # 配置类 26 | self.config = Config() 27 | # 请求中间件 28 | self.request_middleware = [] 29 | # 响应中间件 30 | self.response_middleware = [] 31 | # 蓝图 32 | self.blueprints = {} 33 | self._blueprint_order = [] 34 | 35 | # -------------------------------------------------------------------- # 36 | # Registration 37 | # -------------------------------------------------------------------- # 38 | 39 | # Decorator 40 | def route(self, uri, methods=None): 41 | """ 42 | Decorates a function to be registered as a route 43 | :param uri: path of the URL 44 | :param methods: list or tuple of methods allowed 45 | :return: decorated function 46 | """ 47 | def response(handler): 48 | self.router.add(uri=uri, methods=methods, handler=handler) 49 | return handler 50 | 51 | return response 52 | 53 | # Decorator 54 | def exception(self, *exceptions): 55 | """ 56 | Decorates a function to be registered as a route 57 | :param uri: path of the URL 58 | :param methods: list or tuple of methods allowed 59 | :return: decorated function 60 | """ 61 | 62 | def response(handler): 63 | for exception in exceptions: 64 | self.error_handler.add(exception, handler) 65 | return handler 66 | 67 | return response 68 | 69 | # Decorator 70 | def middleware(self, *args, **kwargs): 71 | """ 72 | Decorates and registers middleware to be called before a request 73 | can either be called as @app.middleware or @app.middleware('request') 74 | """ 75 | attach_to = 'request' 76 | 77 | def register_middleware(middleware): 78 | if attach_to == 'request': 79 | self.request_middleware.append(middleware) 80 | if attach_to == 'response': 81 | self.response_middleware.append(middleware) 82 | return middleware 83 | 84 | # Detect which way this was called, @middleware or @middleware('AT') 85 | if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): 86 | return register_middleware(args[0]) 87 | else: 88 | attach_to = args[0] 89 | return register_middleware 90 | 91 | def register_blueprint(self, blueprint, **options): 92 | """ 93 | Registers a blueprint on the application. 94 | :param blueprint: Blueprint object 95 | :param options: option dictionary with blueprint defaults 96 | :return: Nothing 97 | """ 98 | if blueprint.name in self.blueprints: 99 | assert self.blueprints[blueprint.name] is blueprint, \ 100 | 'A blueprint with the name "%s" is already registered. ' \ 101 | 'Blueprint names must be unique.' % \ 102 | (blueprint.name,) 103 | else: 104 | self.blueprints[blueprint.name] = blueprint 105 | self._blueprint_order.append(blueprint) 106 | blueprint.register(self, options) 107 | 108 | # -------------------------------------------------------------------- # 109 | # Request Handling 110 | # -------------------------------------------------------------------- # 111 | 112 | async def handle_request(self, request, response_callback): 113 | """ 114 | Takes a request from the HTTP Server and returns a response object to be sent back 115 | The HTTP Server only expects a response object, so exception handling must be done here 116 | :param request: HTTP Request object 117 | :param response_callback: Response function to be called with the response as the only argument 118 | :return: Nothing 119 | """ 120 | try: 121 | # Middleware process_request 122 | response = False 123 | # The if improves speed. I don't know why 124 | if self.request_middleware: 125 | for middleware in self.request_middleware: 126 | response = middleware(request) 127 | if isawaitable(response): 128 | response = await response 129 | if response: 130 | break 131 | 132 | # No middleware results 133 | if not response: 134 | # Fetch handler from router 135 | handler, args, kwargs = self.router.get(request) 136 | if handler is None: 137 | raise ServerError("'None' was returned while requesting a handler from the router") 138 | 139 | # Run response handler 140 | response = handler(request, *args, **kwargs) 141 | if isawaitable(response): 142 | response = await response 143 | 144 | # Middleware process_response 145 | if self.response_middleware: 146 | for middleware in self.response_middleware: 147 | _response = middleware(request, response) 148 | if isawaitable(_response): 149 | _response = await _response 150 | if _response: 151 | response = _response 152 | break 153 | 154 | except Exception as e: 155 | try: 156 | response = self.error_handler.response(request, e) 157 | if isawaitable(response): 158 | response = await response 159 | except Exception as e: 160 | if self.debug: 161 | response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc())) 162 | else: 163 | response = HTTPResponse("An error occured while handling an error") 164 | 165 | response_callback(response) 166 | 167 | # -------------------------------------------------------------------- # 168 | # Execution 169 | # -------------------------------------------------------------------- # 170 | 171 | def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, before_stop=None): 172 | """ 173 | Runs the HTTP Server and listens until keyboard interrupt or term signal. 174 | On termination, drains connections before closing. 175 | :param host: Address to host on 176 | :param port: Port to host on 177 | :param debug: Enables debug output (slows server) 178 | :param after_start: Function to be executed after the server starts listening 179 | :param before_stop: Function to be executed when a stop signal is received before it is respected 180 | :return: Nothing 181 | """ 182 | self.error_handler.debug = True 183 | self.debug = debug 184 | 185 | if debug: 186 | log.setLevel(logging.DEBUG) 187 | log.debug(self.config.LOGO) 188 | 189 | # Serve 190 | log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) 191 | 192 | try: 193 | serve( 194 | host=host, 195 | port=port, 196 | debug=debug, 197 | after_start=after_start, 198 | before_stop=before_stop, 199 | request_handler=self.handle_request, 200 | request_timeout=self.config.REQUEST_TIMEOUT, 201 | request_max_size=self.config.REQUEST_MAX_SIZE, 202 | ) 203 | except: 204 | pass 205 | 206 | def stop(self): 207 | """ 208 | This kills the Sanic 209 | """ 210 | asyncio.get_event_loop().stop() 211 | -------------------------------------------------------------------------------- /sanic_0_1_2/src/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from inspect import isawaitable 3 | from signal import SIGINT, SIGTERM 4 | 5 | import httptools 6 | 7 | try: 8 | import uvloop as async_loop 9 | except: 10 | async_loop = asyncio 11 | 12 | from .log import log 13 | from .request import Request 14 | 15 | 16 | class Signal: 17 | stopped = False 18 | 19 | 20 | class HttpProtocol(asyncio.Protocol): 21 | """ 22 | 用来处理与客户端通信 23 | https://pymotw.com/3/asyncio/io_protocol.html 24 | """ 25 | __slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection 26 | 'parser', 'request', 'url', 'headers', # request params 27 | 'request_handler', 'request_timeout', 'request_max_size', # request config 28 | '_total_request_size', '_timeout_handler') # connection management 29 | 30 | def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60, 31 | request_max_size=None): 32 | self.loop = loop 33 | self.transport = None 34 | self.request = None 35 | self.parser = None 36 | self.url = None 37 | self.headers = None 38 | self.signal = signal 39 | self.connections = connections 40 | self.request_handler = request_handler 41 | self.request_timeout = request_timeout 42 | self.request_max_size = request_max_size 43 | self._total_request_size = 0 44 | self._timeout_handler = None 45 | 46 | # -------------------------------------------- # 47 | 48 | # Connection 49 | # -------------------------------------------- # 50 | 51 | def connection_made(self, transport): 52 | """ 53 | 注释:每当有一个新的客户端连接,该方法就会被触发 54 | :param transport: transport 参数是一个 asyncio.Transport 实例对象 55 | :return: 56 | """ 57 | self.connections[self] = True 58 | # 超时时间后执行 connection_timeout 并且关闭 self.transport 对象 59 | self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout) 60 | self.transport = transport 61 | 62 | def connection_lost(self, exc): 63 | """ 64 | 当一个连接被关闭的时候 65 | :param exc: 66 | :return: 67 | """ 68 | del self.connections[self] 69 | self._timeout_handler.cancel() 70 | self.cleanup() 71 | 72 | def connection_timeout(self): 73 | self.bail_out("Request timed out, connection closed") 74 | 75 | # -------------------------------------------- # 76 | 77 | # Parsing 78 | # -------------------------------------------- # 79 | 80 | def data_received(self, data): 81 | """ 82 | 当有数据从客户端发到服务端的时候会使用传输过来的数据调用此方法 83 | :param data: 传输过来的数据 84 | :return: 85 | """ 86 | # Check for the request itself getting too large and exceeding memory limits 87 | self._total_request_size += len(data) 88 | if self._total_request_size > self.request_max_size: 89 | return self.bail_out("Request too large ({}), connection closed".format(self._total_request_size)) 90 | 91 | # Create parser if this is the first time we're receiving data 92 | if self.parser is None: 93 | assert self.request is None 94 | self.headers = [] 95 | self.parser = httptools.HttpRequestParser(self) 96 | 97 | # Parse request chunk or close connection 98 | try: 99 | self.parser.feed_data(data) 100 | except httptools.parser.errors.HttpParserError as e: 101 | self.bail_out("Invalid request data, connection closed ({})".format(e)) 102 | 103 | def on_url(self, url): 104 | self.url = url 105 | 106 | def on_header(self, name, value): 107 | # 继 on_url 后被循环调用 生成self.headers 108 | if name == b'Content-Length' and int(value) > self.request_max_size: 109 | return self.bail_out("Request body too large ({}), connection closed".format(value)) 110 | 111 | self.headers.append((name.decode(), value.decode('utf-8'))) 112 | 113 | def on_headers_complete(self): 114 | # 继 on_header 后被调用 实例化 Request 类 115 | self.request = Request( 116 | url_bytes=self.url, 117 | headers=dict(self.headers), 118 | version=self.parser.get_http_version(), 119 | method=self.parser.get_method().decode() 120 | ) 121 | 122 | def on_body(self, body): 123 | self.request.body = body 124 | 125 | def on_message_complete(self): 126 | # 继 on_headers_complete 后被调用 执行 self.request_handler 最后回调给 self.write_response 127 | self.loop.create_task(self.request_handler(self.request, self.write_response)) 128 | 129 | # -------------------------------------------- # 130 | # Responding 131 | # -------------------------------------------- # 132 | 133 | def write_response(self, response): 134 | # on_message_complete 回调 write_response 返回响应值 135 | # response 为 HTTPResponse 响应对象 包含 content_type body headers status 136 | try: 137 | keep_alive = self.parser.should_keep_alive() and not self.signal.stopped 138 | self.transport.write(response.output(self.request.version, keep_alive, self.request_timeout)) 139 | if not keep_alive: 140 | self.transport.close() 141 | else: 142 | self.cleanup() 143 | except Exception as e: 144 | self.bail_out("Writing request failed, connection closed {}".format(e)) 145 | 146 | def bail_out(self, message): 147 | log.error(message) 148 | # 关闭连接 此时 connection_lost 会被执行 149 | self.transport.close() 150 | 151 | def cleanup(self): 152 | self.parser = None 153 | self.request = None 154 | self.url = None 155 | self.headers = None 156 | self._total_request_size = 0 157 | 158 | def close_if_idle(self): 159 | """ 160 | Close the connection if a request is not being sent or received 161 | :return: boolean - True if closed, false if staying open 162 | """ 163 | if not self.parser: 164 | self.transport.close() 165 | return True 166 | return False 167 | 168 | 169 | def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60, 170 | request_max_size=None): 171 | # Create Event Loop 172 | # 注释: 173 | # 创建一个新的时间循环并返回 174 | loop = async_loop.new_event_loop() 175 | # 注释: 176 | # 为当前上下文设置事件循环 177 | asyncio.set_event_loop(loop) 178 | # I don't think we take advantage of this 179 | # And it slows everything waaayyy down 180 | # loop.set_debug(debug) 181 | 182 | connections = {} 183 | signal = Signal() 184 | # 一个可以创建 TCP 服务的协程 185 | # 每当一个新的客户端建立连接 服务 就会创建一个新的 Protocol 实例 186 | server_coroutine = loop.create_server(lambda: HttpProtocol( 187 | loop=loop, 188 | connections=connections, 189 | signal=signal, 190 | request_handler=request_handler, 191 | request_timeout=request_timeout, 192 | request_max_size=request_max_size, 193 | ), host, port) 194 | try: 195 | # ]> 196 | http_server = loop.run_until_complete(server_coroutine) 197 | except OSError as e: 198 | log.error("Unable to start server: {}".format(e)) 199 | return 200 | except: 201 | log.exception("Unable to start server") 202 | return 203 | 204 | # Run the on_start function if provided 205 | # 注释: 206 | # 服务启动后若after_start满足条件将被执行 207 | if after_start: 208 | result = after_start(loop) 209 | if isawaitable(result): 210 | loop.run_until_complete(result) 211 | 212 | # Register signals for graceful termination 213 | # 注释: 214 | # 调用AbstractEventLoop.add_signal_handler()方法 https://docs.python.org/3/library/asyncio-eventloop.html 215 | # 例:当接受到如Ctrl-c发送的SIGINT会自动调用此时的callback函数loop.stop 216 | for _signal in (SIGINT, SIGTERM): 217 | loop.add_signal_handler(_signal, loop.stop) 218 | 219 | try: 220 | # 注释: 221 | # 运行这个事件循环,以便接收客户端请求以及处理相关事件 222 | # 一直运行,直到loop.close() 223 | loop.run_forever() 224 | finally: 225 | log.info("Stop requested, draining connections...") 226 | 227 | # Run the on_stop function if provided 228 | if before_stop: 229 | result = before_stop(loop) 230 | if isawaitable(result): 231 | loop.run_until_complete(result) 232 | 233 | # Wait for event loop to finish and all connections to drain 234 | http_server.close() 235 | loop.run_until_complete(http_server.wait_closed()) 236 | 237 | # Complete all tasks on the loop 238 | signal.stopped = True 239 | for connection in connections.keys(): 240 | connection.close_if_idle() 241 | 242 | while connections: 243 | loop.run_until_complete(asyncio.sleep(0.1)) 244 | 245 | loop.close() 246 | log.info("Server Stopped") 247 | --------------------------------------------------------------------------------