├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── benchmarks ├── japronto_app.py ├── meinheld_app.py ├── sanic_app.py ├── vibora_app.py └── xweb_app.py ├── example.py ├── logo.png ├── requirements.txt ├── setup.py ├── test_xweb.py └── xweb.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | .pytest_cache 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | .idea 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.6' 4 | sudo: false 5 | install: 6 | - pip install ./ 7 | - pip install -r requirements.txt 8 | script: pytest 9 | deploy: 10 | provider: pypi 11 | distributions: sdist bdist_wheel 12 | user: gaojiuli 13 | password: 14 | secure: l8nnQggmePAgIsgCt3kgdiXUBACThoAOX0hWNRmtdgCLFQKCpoaXw0aEXhQdQDdJ07bdEBxNTk4KBeJLq4sgCY3sr2cJWZY+veIS3+Ycw1OPk8LCGqCHElSrIt9B7bDyGuzyMF7kNvo7YYn9gkb0AJ+HAx6CYT3mJ0tISFv+XwKN0NeS0D4iytwH0OqlSgCGDnHQNThdEF3gn7N6U86SXzYZIrHBlDgfIYSAh8UhcnbQg6EzWf08SEob2oB5vcQnBcnI4aQi5AqLFzrQHYZweWazmhmnlhQr9SPp21YZJZdbwRA83G1dQaqBJRyqT/lepjL5/Q1t88hYg96ASp/I2O1kcVfTrSdctVjIzVDK4afjSRQA2Y/E0+6JT9K1xOZUWkXER+wzzZZwoUWe8mK1SE7B2VocyMrMlU4Pa01zvfJsHR8oFiZr9idUK+IZPkD7Zsb0AUyJ8E2yaD3aD3wbqFvasN2dDuxWGRNaKMp9UM0nUK68zHyeY53gLQMfnEWH3osNRyouAX7jsCaxw20D3oYE2xe2+PT+Rj7AU3NEBFHgVhGEuvhohZEjVNzt6w2Ol93HzwIoIkDXcLmW6lrOUesmjkCYWdmMlTZhlSRfm0vEv6DYVuhDI81s5D13V5nnrPGKQ6JQRkJOx8atx3vm9MmRQZtZvzOrZH+MuPDAFIE= 15 | on: 16 | tags: true 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2018.09.29 2 | 3 | 1. Init -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Gao JiuLi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![[logo](https://github.com/gaojiuli/xweb)](logo.png) 2 | 3 | 4 | ![[Build](https://travis-ci.org/gaojiuli/xweb)](https://travis-ci.org/gaojiuli/xweb.svg?branch=master) 5 | ![[License](https://pypi.python.org/pypi/xweb/)](https://img.shields.io/pypi/l/xweb.svg) 6 | ![[Pypi](https://pypi.python.org/pypi/xweb/)](https://img.shields.io/pypi/v/xweb.svg) 7 | ![[Python](https://pypi.python.org/pypi/xweb/)](https://img.shields.io/pypi/pyversions/xweb.svg) 8 | 9 | High performance web framework. 10 | 11 | ## Installation 12 | 13 | `pip install xweb` 14 | 15 | ## Usage 16 | 17 | ```python 18 | from xweb import App, RESTController 19 | 20 | 21 | class IndexController(RESTController): 22 | async def get(self): 23 | self.ctx.body = {"Hello": "World"} 24 | 25 | 26 | app = App() 27 | app.routes = { 28 | '/': IndexController 29 | } 30 | 31 | app.listen() 32 | ``` -------------------------------------------------------------------------------- /benchmarks/japronto_app.py: -------------------------------------------------------------------------------- 1 | """python japronto_app.py""" 2 | from japronto import Application 3 | 4 | 5 | def hello(request): 6 | return request.Response(text='Hello world!') 7 | 8 | 9 | # 150000 10 | app = Application() 11 | app.router.add_route('/', hello) 12 | app.run(debug=False, worker_num=4, port=8000) 13 | -------------------------------------------------------------------------------- /benchmarks/meinheld_app.py: -------------------------------------------------------------------------------- 1 | """gunicorn -k meinheld.gmeinheld.MeinheldWorker -w4 meinheld_app:app -b 127.0.0.1:8000""" 2 | 3 | 4 | # Requests/sec 77000 5 | def app(environ, start_response): 6 | start_response('200 OK', [('Content-Type', 'text/html')]) 7 | return [b'Hello world'] 8 | -------------------------------------------------------------------------------- /benchmarks/sanic_app.py: -------------------------------------------------------------------------------- 1 | """python sanic_app.py""" 2 | from sanic import Sanic 3 | from sanic.response import text 4 | 5 | app = Sanic() 6 | 7 | 8 | @app.route('/') 9 | async def test(request): 10 | return text('hello world') 11 | 12 | 13 | # Requests/sec 50000 14 | if __name__ == '__main__': 15 | app.run(host='0.0.0.0', port=8000, workers=4, access_log=False) 16 | -------------------------------------------------------------------------------- /benchmarks/vibora_app.py: -------------------------------------------------------------------------------- 1 | """python vibora_app.py""" 2 | from vibora import Request, Vibora 3 | from vibora.responses import Response 4 | 5 | app = Vibora() 6 | 7 | 8 | @app.route('/') 9 | async def home(request: Request): 10 | return Response(b'hello world') 11 | 12 | 13 | # 90000 Requests/sec 14 | if __name__ == '__main__': 15 | app.run(debug=False, host='0.0.0.0', port=8000, workers=4) 16 | -------------------------------------------------------------------------------- /benchmarks/xweb_app.py: -------------------------------------------------------------------------------- 1 | """python xweb_app.py""" 2 | from xweb import App 3 | 4 | app = App() 5 | 6 | 7 | # 100000 Requests/sec 8 | @app.use 9 | async def response(ctx): 10 | ctx.body = "Hello World" 11 | 12 | 13 | app.listen(8000) 14 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | from xweb import App, Model, RESTController 2 | 3 | 4 | class UserModel(Model): 5 | schema = { 6 | "type": "object", 7 | "properties": { 8 | "username": {"type": "string"}, 9 | "password": {"type": "string"}, 10 | }, 11 | "required": ['username'] 12 | } 13 | 14 | 15 | class EventController(RESTController): 16 | async def get(self): 17 | Model.validate(self.ctx.json) 18 | self.ctx.body = {"Hello": "World"} 19 | 20 | 21 | app = App() 22 | app.routes = { 23 | '/': EventController 24 | } 25 | 26 | app.listen() 27 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotgao2/xweb/28a7b1e0d7ca424bf1d4b9265d68a212c5e5a799/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ujson 2 | uvloop 3 | gunicorn 4 | httptools 5 | pytest 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import re 3 | 4 | from setuptools import setup 5 | 6 | _version_re = re.compile(r'__version__\s+=\s+(.*)') 7 | 8 | with open('xweb.py', 'rb') as f: 9 | version = str(ast.literal_eval(_version_re.search( 10 | f.read().decode('utf-8')).group(1))) 11 | 12 | setup(name='xweb', 13 | version=version, 14 | description='High performance web framework built with uvloop and httptools.', 15 | author='gaojiuli', 16 | long_description=open('README.md', 'r', encoding='utf8').read(), 17 | long_description_content_type="text/markdown", 18 | author_email='gaojiuli@gmail.com', 19 | url='https://github.com/gaojiuli/xweb', 20 | py_modules=['xweb'], 21 | install_requires=[ 22 | 'asyncio', 23 | 'ujson', 24 | 'uvloop', 25 | 'httptools', 26 | 'gunicorn', 27 | 'jsonschema' 28 | ], 29 | license='MIT', 30 | platforms='any', 31 | classifiers=['Development Status :: 3 - Alpha', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries', 35 | 'Topic :: Internet :: WWW/HTTP :: HTTP Servers', 36 | 'Topic :: Software Development :: Libraries :: Application Frameworks', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Programming Language :: Python :: 3.7', 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /test_xweb.py: -------------------------------------------------------------------------------- 1 | """ 2 | Todo: add test case. 3 | """ 4 | from xweb import App, Context, Request, Response 5 | 6 | 7 | def test_response(): 8 | assert Response() 9 | 10 | 11 | def test_request(): 12 | assert Request() 13 | 14 | 15 | def test_context(): 16 | assert Context() 17 | 18 | 19 | def test_app(): 20 | assert App() 21 | -------------------------------------------------------------------------------- /xweb.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import multiprocessing 3 | import os 4 | import socket 5 | import ujson as json 6 | from email.utils import formatdate 7 | from functools import partial 8 | from http import HTTPStatus 9 | 10 | import httptools 11 | from jsonschema import Draft4Validator, ErrorTree 12 | 13 | __version__ = '3.0.1' 14 | 15 | 16 | class HTTPException(Exception): 17 | def __init__(self, status, msg=None, properties=None): 18 | self.properties = properties 19 | self.msg = msg 20 | self.status = status 21 | 22 | 23 | class Request: 24 | def __init__(self): 25 | self.headers = {} 26 | self.method = "HEAD" 27 | self.url = "/" 28 | self.raw = None 29 | self.ip = None 30 | 31 | def __getitem__(self, item): 32 | return getattr(self, item) 33 | 34 | def __setitem__(self, key, value): 35 | setattr(self, key, value) 36 | 37 | 38 | class Response: 39 | def __init__(self): 40 | self.body = "" 41 | self.status = 200 42 | self.msg = "" 43 | self.headers = { 44 | 'Date': formatdate(timeval=None, localtime=False, usegmt=True), 45 | 'Content-Type': 'text/plain' 46 | } 47 | 48 | def __getitem__(self, item): 49 | return getattr(self, item) 50 | 51 | def __setitem__(self, key, value): 52 | setattr(self, key, value) 53 | 54 | def __bytes__(self): 55 | http_status = HTTPStatus(self.status) 56 | http_status_bytes = f"HTTP/1.1 {http_status.value} {http_status.phrase}".encode() 57 | http_body_bytes = self.body.encode() 58 | self.headers['Content-Length'] = len(http_body_bytes) 59 | http_header_bytes = "\r\n".join([f'{k}: {v}' for k, v in self.headers.items()]).encode() 60 | return http_status_bytes + b'\r\n' + http_header_bytes + b'\r\n\r\n' + http_body_bytes 61 | 62 | 63 | class Context: 64 | def __init__(self): 65 | self.req = Request() 66 | self.resp = Response() 67 | self.write = None 68 | 69 | def send(self, _): 70 | self.write(bytes(self.resp)) 71 | 72 | def check(self, value, status=400, msg='', properties=""): 73 | if not value: 74 | self.abort(status=status, msg=msg, properties=properties) 75 | 76 | def abort(self, status, msg="", properties=""): 77 | raise HTTPException(status=status, msg=msg, properties=properties) 78 | 79 | def __getattr__(self, item): 80 | return getattr(self.req, item) 81 | 82 | def __getitem__(self, item): 83 | return getattr(self, item) 84 | 85 | def __setitem__(self, key, value): 86 | setattr(self, key, value) 87 | 88 | @property 89 | def headers(self): 90 | return self.resp.headers 91 | 92 | @property 93 | def json(self): 94 | return json.loads(self.body) 95 | 96 | @property 97 | def body(self): 98 | return self.resp.body 99 | 100 | @body.setter 101 | def body(self, value): 102 | self.resp.body = value 103 | 104 | @property 105 | def status(self): 106 | return self.resp.status 107 | 108 | @status.setter 109 | def status(self, value): 110 | self.resp.status = value 111 | 112 | @property 113 | def msg(self): 114 | return self.resp.msg 115 | 116 | @msg.setter 117 | def msg(self, value): 118 | self.resp.msg = value 119 | 120 | 121 | class HTTPProtocol(asyncio.Protocol): 122 | 123 | def __init__(self, handler, loop): 124 | self.parser = None 125 | self.transport = None 126 | self.handler = handler 127 | self.loop = loop 128 | self.ctx = None 129 | 130 | def connection_made(self, transport): 131 | self.parser = httptools.HttpRequestParser(self) 132 | self.transport = transport 133 | 134 | def on_url(self, url): 135 | self.ctx = Context() 136 | self.ctx.write = self.transport.write 137 | url = httptools.parse_url(url) 138 | self.ctx.req.path = url.path.decode() 139 | self.ctx.req.method = self.parser.get_method().decode() 140 | 141 | def on_header(self, name, value): 142 | self.ctx.req.headers[name.decode()] = value.decode() 143 | 144 | def on_body(self, body): 145 | self.ctx.req.raw += body 146 | 147 | def on_message_complete(self): 148 | task = self.loop.create_task(self.handler(self.ctx)) 149 | task.add_done_callback(self.ctx.send) 150 | 151 | def data_received(self, data): 152 | self.parser.feed_data(data) 153 | 154 | def connection_lost(self, exc): 155 | self.transport.close() 156 | 157 | 158 | class App: 159 | def __init__(self): 160 | self.workers = set() 161 | self.routes = {} 162 | 163 | def serve(self, sock): 164 | loop = asyncio.new_event_loop() 165 | server = loop.create_server(partial(HTTPProtocol, loop=loop, handler=self), sock=sock) 166 | loop.create_task(server) 167 | try: 168 | loop.run_forever() 169 | except KeyboardInterrupt: 170 | server.close() 171 | loop.close() 172 | 173 | def listen(self, port=8000, host="127.0.0.1", workers=multiprocessing.cpu_count()): 174 | import uvloop 175 | 176 | asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) 177 | 178 | pid = os.getpid() 179 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 180 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 181 | sock.setblocking(False) 182 | sock.bind((host, port)) 183 | os.set_inheritable(sock.fileno(), True) 184 | 185 | try: 186 | print(f'[{pid}] Listening at: http://{host}:{port}') 187 | print(f'[{pid}] Workers: {workers}') 188 | for _ in range(workers): 189 | worker = multiprocessing.Process(target=self.serve, kwargs=dict(sock=sock)) 190 | worker.daemon = True 191 | worker.start() 192 | print(f'[{pid}] Starting worker with pid: {worker.pid}') 193 | self.workers.add(worker) 194 | for worker in self.workers: 195 | worker.join() 196 | except KeyboardInterrupt: 197 | print('\r', end='\r') 198 | print(f'[{pid}] Server soft stopping') 199 | for worker in self.workers: 200 | worker.terminate() 201 | worker.join() 202 | print(f'[{pid}] Server stopped successfully!') 203 | sock.close() 204 | 205 | async def __call__(self, ctx): 206 | try: 207 | handler = self.routes.get(ctx.req.path) 208 | if not handler: 209 | raise HTTPException(404) 210 | await handler(ctx).request() 211 | except HTTPException as e: 212 | ctx.status = e.status 213 | ctx.body = e.msg or HTTPStatus(e.status).phrase 214 | ctx.msg = e.properties 215 | 216 | 217 | class Controller: 218 | def __init__(self, ctx): 219 | self.ctx = ctx 220 | 221 | async def request(self): 222 | handler = getattr(self, self.ctx.req.method.lower(), None) 223 | if not handler: 224 | raise HTTPException(405) 225 | await handler() 226 | 227 | 228 | class RESTController(Controller): 229 | 230 | async def request(self): 231 | self.ctx.headers['Content-Type'] = 'application/json' 232 | await super().request() 233 | self.ctx.body = json.dumps(self.ctx.body) 234 | 235 | 236 | class Model: 237 | schema = {} 238 | 239 | @classmethod 240 | def validate(cls, data): 241 | errors = ErrorTree(Draft4Validator(cls.schema).iter_errors(data)).errors 242 | if errors: 243 | raise HTTPException(400, msg=str(errors)) 244 | return data 245 | --------------------------------------------------------------------------------