├── .gitignore ├── LICENSE ├── README.md ├── demo ├── README.md ├── app │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── handlers.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── urls.py ├── application.py ├── core │ ├── __init__.py │ ├── base.py │ ├── handlers.py │ └── monkey_patch.py ├── demo │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── requirements.txt ├── greentor ├── __init__.py ├── glocal.py ├── green.py └── mysql.py ├── setup.py └── tests ├── hello.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.pyc 3 | dist/ 4 | *.egg-info/ 5 | *.swp 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alexzhang 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## greentor 2 | 3 | **greentor** is a fork of [gTornado](https://github.com/alex8224/gTornado) 4 | 5 | greentor通过给pymysql打补丁,使pymysql在Tornado中的运行过程变为异步IO,相比于其它支持Tornado的mysql驱动,greentor有以下不同 6 | 7 | 1. 同步pymysql的写法 8 | 2. 理论上可以支持各种ORM的调用异步 9 | 10 | 感谢[@alex8224](https://github.com/alex8224)和他的[gTornado](https://github.com/alex8224/gTornado) 11 | 12 | 感谢[@snower](https://github.com/snower),参考他的[TorMySQL](https://github.com/snower/TorMySQL)优化了IOStream的读写性能 13 | 14 | ### 安装 15 | 16 | ```shell 17 | pip install git+https://github.com/zhu327/greentor.git 18 | ``` 19 | 20 | ### 使用 21 | 22 | ```python 23 | # coding: utf-8 24 | 25 | from greentor import green 26 | green.enable_debug() 27 | from greentor import mysql 28 | mysql.patch_pymysql() 29 | ``` 30 | 31 | 1. `green.enable_debug()` 32 | 非必须,开启greenlet调试模式,可以打印greenlet switch过程 33 | 2. `mysql.patch_pymysql()` 34 | 给pymysql打异步补丁,异步的pymysql依赖于Tornado,在Tornado的IOLoop start后才能正常使用 35 | 36 | #### 在`RequestHandler`中使用 37 | 38 | 涉及到pymysql的调用都需要运行在greenlet中,提供了3种方式实现同步代码转异步 39 | 40 | ```python 41 | from greentor import green 42 | from greentor import mysql 43 | mysql.patch_pymysql() 44 | import tornado.web 45 | 46 | class MainHandler(tornado.web.RequestHandler): 47 | @green.green 48 | def get(self): 49 | connect = MySQLdb.connect(user='root', 50 | passwd='', 51 | db='test', 52 | host='localhost', 53 | port=3306) 54 | cursor = connect.cursor() 55 | cursor.execute('SELECT * FROM app_blog LIMIT 1') 56 | result = cursor.fetchone() 57 | cursor.close() 58 | connect.close() 59 | self.finish(result[2]) 60 | ``` 61 | 62 | 通过`green.green`装饰器使整个get方法都运行在greenlet中,这样是最方便的使用pymysql的方式 63 | 64 | ```python 65 | from greentor import green 66 | from greentor import mysql 67 | mysql.patch_pymysql() 68 | import tornado.web 69 | import tornado.gen 70 | 71 | @green.green 72 | def test_mysql(): 73 | connect = MySQLdb.connect(user='root', 74 | passwd='', 75 | db='test', 76 | host='localhost', 77 | port=3306) 78 | cursor = connect.cursor() 79 | cursor.execute('SELECT * FROM app_blog LIMIT 1') 80 | result = cursor.fetchone() 81 | cursor.close() 82 | connect.close() 83 | return result 84 | 85 | 86 | class MainHandler(tornado.web.RequestHandler): 87 | @tornado.gen.coroutine 88 | def get(self): 89 | result = yield test_mysql() 90 | self.finish(result[2]) 91 | ``` 92 | 93 | 通过`green.green`装饰器包装的函数会返回`Future`对象,可以在Tornado的协程中使用 94 | 95 | ```python 96 | from greentor import green 97 | from greentor import mysql 98 | mysql.patch_pymysql() 99 | import tornado.web 100 | import tornado.gen 101 | 102 | def test_mysql(): 103 | connect = MySQLdb.connect(user='root', 104 | passwd='', 105 | db='test', 106 | host='localhost', 107 | port=3306) 108 | cursor = connect.cursor() 109 | cursor.execute('SELECT * FROM app_blog LIMIT 1') 110 | result = cursor.fetchone() 111 | cursor.close() 112 | connect.close() 113 | return result 114 | 115 | 116 | class MainHandler(tornado.web.RequestHandler): 117 | @tornado.gen.coroutine 118 | def get(self): 119 | result = yield green.spawn(test_mysql) 120 | self.finish(result[2]) 121 | ``` 122 | 123 | `green.spawn(callable_obj, *arg, **kwargs)`的调用与`green.green`一致 124 | 125 | ### 实例 126 | 127 | 在tests目录下有一个使用纯pymysql调用的实例 128 | 129 | demo目录下有一个完整的 Tornado + Django ORM 的环境,具体可以查看demo目录下的[README](https://github.com/zhu327/greentor/tree/master/demo) 130 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | ## Tornado + Django ORM 2 | 3 | 这是一个 Tornado + Django ORM 运行环境的demo 4 | 5 | demo目录是Django的配置文件目录,app目录是Django的app 6 | 7 | 不同于Django app目录下的views.py,我将其重命名为handlers.py 8 | 9 | ### 运行 10 | 11 | ```shell 12 | python application.py 13 | ``` 14 | 15 | 在运行tornado的同时,Django admin也是可以访问的 16 | 17 | ### 说明 18 | 19 | 主要的补丁都在core目录下 20 | 21 | ``` 22 | . 23 | ├── __init__.py 24 | ├── base.py // Django mysql connection pool backend 连接池支持 25 | ├── handlers.py // BaseRequestHandler 基类 26 | └── monkey_patch.py // 一系列的补丁 27 | ``` 28 | 29 | ```python 30 | import tornado.web 31 | import tornado.gen 32 | from greentor import green 33 | 34 | from .models import Blog 35 | 36 | 37 | class BlogHandler(tornado.web.RequestHandler): 38 | @tornado.gen.coroutine 39 | def get(self): 40 | blogs = yield green.spawn(Blog.objects.all) 41 | self.write(blogs) 42 | ``` 43 | 44 | 这样使用是会报错的,因为返回的blogs是QuerySet对象是Lazy的,需要调用**_fetch_all**方法才执行了SQL查询 45 | 46 | ### 扩展 47 | 48 | 在Tornado中使用Django session user和Django request对象也是可行的,可以参考: 49 | 50 | 51 | 52 | 当然需要查询数据库的地方都要加上`green.green`装饰器 53 | -------------------------------------------------------------------------------- /demo/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhu327/greentor/2960658d891672d8f016a7f88e4f737e35e22e0b/demo/app/__init__.py -------------------------------------------------------------------------------- /demo/app/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | from .models import Blog 5 | 6 | admin.site.register(Blog) -------------------------------------------------------------------------------- /demo/app/apps.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.apps import AppConfig 4 | 5 | 6 | class AppConfig(AppConfig): 7 | name = 'app' -------------------------------------------------------------------------------- /demo/app/handlers.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import tornado.web 4 | 5 | from core.handlers import BaseRequestHandler 6 | from .models import Blog 7 | 8 | 9 | class HelloHandler(tornado.web.RequestHandler): 10 | def get(self): 11 | self.write('Hello, world ! \n') 12 | 13 | 14 | class BlogHandler(BaseRequestHandler): 15 | def get(self): 16 | blog = Blog.objects.first() 17 | self.finish(blog.content) 18 | -------------------------------------------------------------------------------- /demo/app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.7 on 2016-06-20 16:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Blog', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('title', models.CharField(max_length=100, verbose_name='\u6807\u9898')), 21 | ('content', models.TextField(blank=True, null=True, verbose_name='\u6b63\u6587')), 22 | ('add_dt', models.DateTimeField(auto_now_add=True, null=True, verbose_name='\u6dfb\u52a0\u65f6\u95f4')), 23 | ], 24 | options={ 25 | 'verbose_name': 'Blog', 26 | 'verbose_name_plural': 'Blogs', 27 | }, 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /demo/app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhu327/greentor/2960658d891672d8f016a7f88e4f737e35e22e0b/demo/app/migrations/__init__.py -------------------------------------------------------------------------------- /demo/app/models.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import unicode_literals 4 | 5 | from django.db import models 6 | 7 | # Create your models here. 8 | 9 | class Blog(models.Model): 10 | u'''博客测试 11 | ''' 12 | title = models.CharField(u'标题', max_length=100) 13 | content = models.TextField(u'正文', null=True, blank=True) 14 | add_dt = models.DateTimeField('添加时间', blank=True, null=True, auto_now_add=True) 15 | 16 | class Meta: 17 | verbose_name = "Blog" 18 | verbose_name_plural = "Blogs" 19 | 20 | def __unicode__(self): 21 | return self.title 22 | -------------------------------------------------------------------------------- /demo/app/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /demo/app/urls.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from handlers import HelloHandler, BlogHandler 4 | 5 | urls=[ 6 | (r"/", HelloHandler), 7 | (r"/blog/", BlogHandler), 8 | ] -------------------------------------------------------------------------------- /demo/application.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import core.monkey_patch 5 | 6 | import os 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 8 | import django 9 | django.setup() 10 | 11 | from tornado.options import options, define, parse_command_line 12 | import tornado.httpserver 13 | import tornado.ioloop 14 | import tornado.wsgi 15 | import tornado.web 16 | 17 | import django.core.handlers.wsgi 18 | from django.conf import settings 19 | 20 | import app.urls 21 | 22 | define('port', type=int, default=8000) 23 | 24 | tornado_settings = {'debug': settings.DEBUG} 25 | 26 | 27 | def main(): 28 | parse_command_line() 29 | 30 | wsgi_app = tornado.wsgi.WSGIContainer( 31 | django.core.handlers.wsgi.WSGIHandler()) 32 | 33 | urls = app.urls.urls + [('.*', tornado.web.FallbackHandler, dict( 34 | fallback=wsgi_app)), ] 35 | 36 | tornado_app = tornado.web.Application(urls, **tornado_settings) 37 | server = tornado.httpserver.HTTPServer(tornado_app) 38 | server.listen(options.port) 39 | tornado.ioloop.IOLoop.instance().start() 40 | 41 | 42 | if __name__ == '__main__': 43 | main() 44 | -------------------------------------------------------------------------------- /demo/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhu327/greentor/2960658d891672d8f016a7f88e4f737e35e22e0b/demo/core/__init__.py -------------------------------------------------------------------------------- /demo/core/base.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from django.db.backends.mysql.base import ( 4 | SafeText, SafeBytes, six, DatabaseWrapper as BaseDatabaseWrapper) 5 | from greentor.mysql import ConnectionPool 6 | 7 | 8 | class DatabaseWrapper(BaseDatabaseWrapper): 9 | u""" 10 | 支持greentor mysql connection pool 的backends 11 | """ 12 | pools = {} 13 | 14 | def get_new_connection(self, conn_params): 15 | # conn = Database.connect(**conn_params) 16 | if not self.alias in self.pools: 17 | self.pools[self.alias] = ConnectionPool( 18 | max_size=conn_params.pop('MAX_SIZE', 32), 19 | keep_alive=conn_params.pop('KEEP_ALIVE', 7200), 20 | mysql_params=conn_params) 21 | conn = self.pools[self.alias].get_conn() 22 | conn.encoders[SafeText] = conn.encoders[six.text_type] 23 | conn.encoders[SafeBytes] = conn.encoders[bytes] 24 | return conn 25 | 26 | def _close(self): 27 | if self.connection is not None: 28 | self.pools[self.alias].release(self.connection) 29 | -------------------------------------------------------------------------------- /demo/core/handlers.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import tornado.web 4 | from django.db import connections 5 | from greentor import green 6 | 7 | 8 | class BaseRequestHandler(tornado.web.RequestHandler): 9 | def on_finish(self): 10 | connections.close_all() 11 | 12 | @green.green 13 | def _execute(self, *args, **kwargs): 14 | return super(BaseRequestHandler, self)._execute(*args, **kwargs) 15 | -------------------------------------------------------------------------------- /demo/core/monkey_patch.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from greentor import mysql 4 | # pymysql打上异步补丁 5 | mysql.patch_pymysql() 6 | import pymysql 7 | pymysql.install_as_MySQLdb() 8 | 9 | from greentor import green 10 | import tornado.wsgi 11 | 12 | # 包装wsgi app运行在greenlet中,使Django admin支持异步pymysql 13 | tornado.wsgi.WSGIContainer.__call__ = green.green( 14 | tornado.wsgi.WSGIContainer.__call__) 15 | 16 | from greentor.glocal import local 17 | from django.db.utils import ConnectionHandler as BaseConnectionHandler 18 | 19 | 20 | class ConnectionHandler(BaseConnectionHandler): 21 | def __init__(self, databases=None): 22 | self._databases = databases 23 | self._connections = local() 24 | 25 | 26 | import django.db 27 | # 使用greenlet local替换threading local,避免threading safe问题 28 | setattr(django.db, 'connections', ConnectionHandler()) 29 | -------------------------------------------------------------------------------- /demo/demo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhu327/greentor/2960658d891672d8f016a7f88e4f737e35e22e0b/demo/demo/__init__.py -------------------------------------------------------------------------------- /demo/demo/settings.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | Django settings for demo project. 4 | 5 | Generated by 'django-admin startproject' using Django 1.9.7. 6 | 7 | For more information on this file, see 8 | https://docs.djangoproject.com/en/1.9/topics/settings/ 9 | 10 | For the full list of settings and their values, see 11 | https://docs.djangoproject.com/en/1.9/ref/settings/ 12 | """ 13 | 14 | import os 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'dih$m)bdeaz=i58uinnmuo)zhjci-s!&po7=1f5_igir_$-5g0' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = [ 33 | 'django.contrib.admin', 34 | 'django.contrib.auth', 35 | 'django.contrib.contenttypes', 36 | 'django.contrib.sessions', 37 | 'django.contrib.messages', 38 | 'django.contrib.staticfiles', 39 | 'app', 40 | 'core', 41 | ] 42 | 43 | MIDDLEWARE_CLASSES = [ 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'demo.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'demo.wsgi.application' 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'NAME': 'test', 80 | 'HOST': '127.0.0.1', 81 | # 'ENGINE': 'django.db.backends.mysql', 82 | 'ENGINE': 'core', 83 | 'USER': 'root', 84 | 'PASSWORD': '', 85 | 'OPTIONS': { 86 | 'MAX_SIZE': 32, # 连接池最大连接数 87 | 'KEEP_ALIVE': 7200, # 连接保持ping周期 88 | }, 89 | } 90 | } 91 | 92 | # Password validation 93 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 94 | 95 | AUTH_PASSWORD_VALIDATORS = [ 96 | { 97 | 'NAME': 98 | 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 99 | }, 100 | { 101 | 'NAME': 102 | 'django.contrib.auth.password_validation.MinimumLengthValidator', 103 | }, 104 | { 105 | 'NAME': 106 | 'django.contrib.auth.password_validation.CommonPasswordValidator', 107 | }, 108 | { 109 | 'NAME': 110 | 'django.contrib.auth.password_validation.NumericPasswordValidator', 111 | }, 112 | ] 113 | 114 | # Internationalization 115 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 116 | 117 | LANGUAGE_CODE = 'zh-hans' 118 | 119 | TIME_ZONE = 'Asia/Shanghai' 120 | 121 | USE_I18N = True 122 | 123 | USE_L10N = True 124 | 125 | USE_TZ = False 126 | 127 | DATE_FORMAT = 'Y-m-d' 128 | 129 | DATETIME_FORMAT = 'Y-m-d H:i:s' 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 133 | 134 | STATIC_URL = '/static/' 135 | -------------------------------------------------------------------------------- /demo/demo/urls.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """demo URL Configuration 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.conf.urls import url, include 15 | 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 16 | """ 17 | import os 18 | import django 19 | from django.conf.urls import url 20 | from django.contrib import admin 21 | from django.views import static 22 | 23 | MEDIA_ROOT = os.path.join(os.path.dirname(django.__file__), 'contrib', 'admin', 24 | 'static', 'admin') 25 | 26 | admin.autodiscover() 27 | 28 | urlpatterns = [ 29 | url(r'^static/admin/(?P.*)$', static.serve, {'document_root': MEDIA_ROOT, 'show_indexes': False}), 30 | url(r'^admin/', admin.site.urls), 31 | ] -------------------------------------------------------------------------------- /demo/demo/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for demo project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /demo/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import pymysql 5 | pymysql.install_as_MySQLdb() 6 | 7 | if __name__ == "__main__": 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings") 9 | 10 | from django.core.management import execute_from_command_line 11 | 12 | execute_from_command_line(sys.argv) 13 | -------------------------------------------------------------------------------- /demo/requirements.txt: -------------------------------------------------------------------------------- 1 | Django==1.9.7 2 | greenlet==0.4.10 3 | PyMySQL==0.7.4 4 | tornado==4.3 5 | git+https://github.com/zhu327/greentor.git 6 | -------------------------------------------------------------------------------- /greentor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhu327/greentor/2960658d891672d8f016a7f88e4f737e35e22e0b/greentor/__init__.py -------------------------------------------------------------------------------- /greentor/glocal.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from greenlet import getcurrent 4 | 5 | __all__ = ('local',) 6 | 7 | 8 | def _get_local_dict(): 9 | current = getcurrent() 10 | s = '_%s__local_dict__' % current.__class__.__name__ 11 | if not hasattr(current, s): 12 | setattr(current, s, {}) 13 | return getattr(current, s) 14 | 15 | 16 | class local(object): 17 | def __getattribute__(self, attr): 18 | local_dict = _get_local_dict() 19 | try: 20 | return local_dict[attr] 21 | except KeyError: 22 | raise AttributeError("'local' object has no attribute '%s'" % attr) 23 | 24 | def __setattr__(self, attr, value): 25 | local_dict = _get_local_dict() 26 | local_dict[attr] = value 27 | 28 | def __delattr__(self, attr): 29 | local_dict = _get_local_dict() 30 | try: 31 | del local_dict[attr] 32 | except KeyError: 33 | raise AttributeError(attr) -------------------------------------------------------------------------------- /greentor/green.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | import sys 5 | import socket 6 | import time 7 | import errno 8 | import greenlet 9 | from functools import wraps 10 | from collections import deque 11 | try: 12 | from StringIO import StringIO 13 | except ImportError: 14 | from io import StringIO 15 | 16 | from tornado.ioloop import IOLoop 17 | from tornado.concurrent import Future 18 | from tornado.gen import coroutine, Return 19 | from tornado.netutil import Resolver 20 | from tornado.iostream import (IOStream as BaseIOStream, StreamClosedError, 21 | _ERRNO_WOULDBLOCK) 22 | 23 | IS_PYPY = False 24 | try: 25 | import __pypy__ 26 | __pypy__ 27 | IS_PYPY = True 28 | except: 29 | pass 30 | 31 | 32 | def enable_debug(): 33 | if IS_PYPY: 34 | sys.stderr.write("settrace api unsupported on pypy") 35 | sys.stderr.flush() 36 | return 37 | 38 | import inspect 39 | 40 | def trace_green(event, args): 41 | src, target = args 42 | if event == "switch": 43 | print("from %s switch to %s" % (src, target)) 44 | elif event == "throw": 45 | print("from %s throw exception to %s" % (src, target)) 46 | 47 | if src.gr_frame: 48 | tracebacks = inspect.getouterframes(src.gr_frame) 49 | buff = [] 50 | for traceback in tracebacks: 51 | srcfile, lineno, func_name, codesample = traceback[1:-1] 52 | trace_line = '''File "%s", line %d, in %s\n%s ''' 53 | buff.append(trace_line % 54 | (srcfile, lineno, func_name, "".join(codesample))) 55 | 56 | print("".join(buff)) 57 | 58 | greenlet.settrace(trace_green) 59 | 60 | 61 | class GreenTask(greenlet.greenlet): 62 | def __init__(self, run, *args, **kwargs): 63 | super(GreenTask, self).__init__() 64 | self._run = run 65 | self._args = args 66 | self._kwargs = kwargs 67 | self._future = Future() 68 | self._result = None 69 | self._exc_info = () 70 | 71 | @property 72 | def args(self): 73 | return self._args 74 | 75 | @property 76 | def kwargs(self): 77 | return self._kwargs 78 | 79 | def run(self): 80 | try: 81 | timeout = self.kwargs.pop("timeout", 0) 82 | if timeout: 83 | timer = Timeout(timeout) 84 | timer.start() 85 | self._result = self._run(*self.args, **self.kwargs) 86 | self._future.set_result(self._result) 87 | except: 88 | self._exc_info = sys.exc_info() 89 | self._future.set_exc_info(self._exc_info) 90 | finally: 91 | if timeout: 92 | timer.cancel() 93 | 94 | def start(self): 95 | self.switch() 96 | 97 | def __str__(self): 98 | func_name = "%s of %s " % (self._run.__name__, self._run.__module__) 99 | return "" % (func_name, hex(id(self))) 100 | 101 | def __repr__(self): 102 | return self.__str__() 103 | 104 | def wait(self): 105 | return self._future 106 | 107 | @classmethod 108 | def spawn(cls_green, *args, **kwargs): 109 | task = cls_green(*args, **kwargs) 110 | task.start() 111 | return task 112 | 113 | 114 | def synclize(func): 115 | coro = coroutine(func) 116 | 117 | @wraps(func) 118 | def _sync_call(*args, **kwargs): 119 | child_gr = greenlet.getcurrent() 120 | main = child_gr.parent 121 | assert main, "only run in child greenlet" 122 | 123 | def callback(future): 124 | if future.exc_info(): 125 | child_gr.throw(*future.exc_info()) 126 | elif future.exception(): 127 | child_gr.throw(future.exception()) 128 | else: 129 | child_gr.switch(future.result()) 130 | 131 | IOLoop.current().add_future(coro(*args, **kwargs), callback) 132 | return main.switch() 133 | 134 | return _sync_call 135 | 136 | 137 | def spawn(callable_obj, *args, **kwargs): 138 | return GreenTask.spawn(callable_obj, *args, **kwargs).wait() 139 | 140 | 141 | def green(func): 142 | @wraps(func) 143 | def wrapper(*args, **kwargs): 144 | return GreenTask.spawn(func, *args, **kwargs).wait() 145 | 146 | return wrapper 147 | 148 | 149 | class Waiter(object): 150 | def __init__(self): 151 | self._greenlet = greenlet.getcurrent() 152 | self._main = self._greenlet.parent 153 | 154 | @property 155 | def greenlet(self): 156 | return self._greenlet 157 | 158 | def switch(self, value): 159 | self._greenlet.switch(value) 160 | 161 | def throw(self, *exc_info): 162 | self._greenlet.throw(*exc_info) 163 | 164 | def get(self): 165 | return self._main.switch() 166 | 167 | def clear(self): 168 | pass 169 | 170 | 171 | def sleep(seconds): 172 | waiter = Waiter() 173 | unique = object() 174 | IOLoop.current().add_timeout(time.time() + seconds, waiter.switch, unique) 175 | waiter.get() 176 | 177 | 178 | class TimeoutException(Exception): 179 | 180 | pass 181 | 182 | 183 | class Timeout(object): 184 | def __init__(self, deadline, ex=TimeoutException): 185 | self._greenlet = greenlet.getcurrent() 186 | self._ex = ex 187 | self._callback = None 188 | self._deadline = deadline 189 | self._delta = time.time() + deadline 190 | self._ioloop = IOLoop.current() 191 | 192 | def start(self, callback=None): 193 | errmsg = "%s timeout, deadline is %d seconds" % (str(self._greenlet), 194 | self._deadline) 195 | if callback: 196 | self._callback = self._ioloop.add_timeout(self._delta, callback, 197 | self._ex(errmsg)) 198 | else: 199 | self._callback = self._ioloop.add_timeout( 200 | self._delta, self._greenlet.throw, self._ex(errmsg)) 201 | 202 | def cancel(self): 203 | assert self._callback, "Timeout not started" 204 | self._ioloop.remove_timeout(self._callback) 205 | self._greenlet = None 206 | 207 | 208 | class IOStream(BaseIOStream): 209 | def _handle_events(self, fd, events): 210 | if self._closed: 211 | return 212 | try: 213 | if self._connecting: 214 | self._handle_connect() 215 | if self._closed: 216 | return 217 | if events & self.io_loop.READ: 218 | self._handle_read() 219 | if self._closed: 220 | return 221 | if events & self.io_loop.WRITE: 222 | self._handle_write() 223 | if self._closed: 224 | return 225 | if events & self.io_loop.ERROR: 226 | self.error = self.get_fd_error() 227 | self.io_loop.add_callback(self.close) 228 | return 229 | except Exception: 230 | self.close(exc_info=True) 231 | raise 232 | 233 | def _handle_connect(self): 234 | super(IOStream, self)._handle_connect() 235 | 236 | if not self.closed(): 237 | self._state = self.io_loop.ERROR | self.io_loop.READ 238 | self.io_loop.update_handler(self.fileno(), self._state) 239 | 240 | def _handle_read(self): 241 | chunk = True 242 | 243 | while True: 244 | try: 245 | chunk = self.socket.recv(self.read_chunk_size) 246 | if not chunk: 247 | break 248 | self._read_buffer.append(chunk) 249 | self._read_buffer_size += len(chunk) 250 | except (socket.error, IOError, OSError) as e: 251 | en = e.errno if hasattr(e, 'errno') else e.args[0] 252 | if en in _ERRNO_WOULDBLOCK: 253 | break 254 | 255 | if en == errno.EINTR: 256 | continue 257 | 258 | self.close(exc_info=True) 259 | return 260 | 261 | if self._read_future is not None and self._read_buffer_size >= self._read_bytes: 262 | future, self._read_future = self._read_future, None 263 | data = b"".join(self._read_buffer) 264 | self._read_buffer.clear() 265 | self._read_buffer_size = 0 266 | self._read_bytes = 0 267 | future.set_result(data) 268 | 269 | if not chunk: 270 | self.close() 271 | return 272 | 273 | def read(self, num_bytes): 274 | assert self._read_future is None, "Already reading" 275 | if self._closed: 276 | raise StreamClosedError(real_error=self.error) 277 | 278 | future = self._read_future = Future() 279 | self._read_bytes = num_bytes 280 | self._read_partial = False 281 | if self._read_buffer_size >= self._read_bytes: 282 | future, self._read_future = self._read_future, None 283 | data = b"".join(self._read_buffer) 284 | self._read_buffer.clear() 285 | self._read_buffer_size = 0 286 | self._read_bytes = 0 287 | future.set_result(data) 288 | return future 289 | 290 | read_bytes = read 291 | 292 | def _handle_write(self): 293 | while self._write_buffer: 294 | try: 295 | data = self._write_buffer.popleft() 296 | num_bytes = self.socket.send(data) 297 | self._write_buffer_size -= num_bytes 298 | if num_bytes < len(data): 299 | self._write_buffer.appendleft(data[num_bytes:]) 300 | return 301 | except (socket.error, IOError, OSError) as e: 302 | en = e.errno if hasattr(e, 'errno') else e.args[0] 303 | if en in _ERRNO_WOULDBLOCK: 304 | self._write_buffer.appendleft(data) 305 | break 306 | 307 | self.close(exc_info=True) 308 | return 309 | 310 | if not self._write_buffer: 311 | if self._state & self.io_loop.WRITE: 312 | self._state = self._state & ~self.io_loop.WRITE 313 | self.io_loop.update_handler(self.fileno(), self._state) 314 | 315 | def write(self, data): 316 | assert isinstance(data, bytes) 317 | if self._closed: 318 | raise StreamClosedError(real_error=self.error) 319 | 320 | if data: 321 | self._write_buffer.append(data) 322 | self._write_buffer_size += len(data) 323 | 324 | if not self._connecting: 325 | self._handle_write() 326 | if self._write_buffer: 327 | if not self._state & self.io_loop.WRITE: 328 | self._state = self._state | self.io_loop.WRITE 329 | self.io_loop.update_handler(self.fileno(), self._state) 330 | 331 | 332 | class AsyncSocket(object): 333 | def __init__(self, sock): 334 | self._iostream = IOStream(sock) 335 | self._resolver = Resolver() 336 | self._readtimeout = 0 337 | self._connecttimeout = 0 338 | self._rbuffer = StringIO(b'') 339 | self._rbuffer_size = 0 340 | 341 | def set_readtimeout(self, timeout): 342 | self._readtimeout = timeout 343 | 344 | def set_connecttimeout(self, timeout): 345 | self._connecttimeout = timeout 346 | 347 | @synclize 348 | def connect(self, address): 349 | host, port = address 350 | timer = None 351 | try: 352 | if self._connecttimeout: 353 | timer = Timeout(self._connecttimeout) 354 | timer.start() 355 | resolved_addrs = yield self._resolver.resolve( 356 | host, 357 | port, 358 | family=socket.AF_INET) 359 | for addr in resolved_addrs: 360 | family, host_port = addr 361 | yield self._iostream.connect(host_port) 362 | break 363 | except TimeoutException as e: 364 | self.close() 365 | raise socket.timeout(e.message) 366 | finally: 367 | if timer: 368 | timer.cancel() 369 | 370 | def sendall(self, buff): 371 | self._iostream.write(buff) 372 | 373 | def read(self, nbytes): 374 | if nbytes <= self._rbuffer_size: 375 | self._rbuffer_size -= nbytes 376 | return self._rbuffer.read(nbytes) 377 | 378 | if self._rbuffer_size > 0: 379 | self._iostream._read_buffer.appendleft(self._rbuffer.read()) 380 | self._iostream._read_buffer_size += self._rbuffer_size 381 | self._rbuffer_size = 0 382 | 383 | if nbytes <= self._iostream._read_buffer_size: 384 | data, data_len = b''.join( 385 | self._iostream._read_buffer), self._iostream._read_buffer_size 386 | self._iostream._read_buffer.clear() 387 | self._iostream._read_buffer_size = 0 388 | 389 | if data_len == nbytes: 390 | return data 391 | 392 | self._rbuffer_size = data_len - nbytes 393 | self._rbuffer = StringIO(data) 394 | return self._rbuffer.read(nbytes) 395 | 396 | data = self._read(nbytes) 397 | if len(data) == nbytes: 398 | return data 399 | 400 | self._rbuffer_size = len(data) - nbytes 401 | self._rbuffer = StringIO(data) 402 | return self._rbuffer.read(nbytes) 403 | 404 | @synclize 405 | def _read(self, nbytes): 406 | timer = None 407 | try: 408 | if self._readtimeout: 409 | timer = Timeout(self._readtimeout) 410 | timer.start() 411 | data = yield self._iostream.read_bytes(nbytes) 412 | raise Return(data) 413 | except TimeoutException as e: 414 | self.close() 415 | raise socket.timeout(e.message) 416 | finally: 417 | if timer: 418 | timer.cancel() 419 | 420 | def recv(self, nbytes): 421 | return self.read(nbytes) 422 | 423 | def close(self): 424 | self._iostream.close() 425 | 426 | def set_nodelay(self, flag): 427 | self._iostream.set_nodelay(flag) 428 | 429 | def settimeout(self, timeout): 430 | pass 431 | 432 | def shutdown(self, direction): 433 | if self._iostream.fileno(): 434 | self._iostream.fileno().shutdown(direction) 435 | 436 | def recv_into(self, buff): 437 | expected_rbytes = len(buff) 438 | data = self.read(expected_rbytes) 439 | srcarray = bytearray(data) 440 | nbytes = len(srcarray) 441 | buff[0:nbytes] = srcarray 442 | return nbytes 443 | 444 | def makefile(self, mode, other): 445 | return self 446 | 447 | def fileno(self): 448 | return self._iostream.fileno() 449 | 450 | 451 | class Event(object): 452 | def __init__(self): 453 | self._waiter = [] 454 | self._ioloop = IOLoop.current() 455 | 456 | def set(self): 457 | self._ioloop.add_callback(self._notify) 458 | 459 | def wait(self, timeout=None): 460 | current_greenlet = greenlet.getcurrent() 461 | self._waiter.append(current_greenlet.switch) 462 | waiter = Waiter() 463 | if timeout: 464 | timeout_checker = Timeout(timeout) 465 | timeout_checker.start(current_greenlet.throw) 466 | waiter.get() 467 | timeout_checker.cancel() 468 | else: 469 | waiter.get() 470 | 471 | def _notify(self): 472 | for waiter in self._waiter: 473 | waiter(self) 474 | 475 | 476 | class Pool(object): 477 | def __init__(self, max_size=32, wait_timeout=8, params={}): 478 | self._maxsize = max_size 479 | self._conn_params = params 480 | self._pool = deque(maxlen=self._maxsize) 481 | self._wait = deque() 482 | self._wait_timeout = wait_timeout 483 | self._count = 0 484 | self._started = False 485 | self._ioloop = IOLoop.current() 486 | self._event = Event() 487 | self._ioloop.add_future(spawn(self.start), lambda future: future) 488 | 489 | def create_raw_conn(self): 490 | pass 491 | 492 | def init_pool(self): 493 | self._count += 1 494 | conn = self.create_raw_conn() 495 | self._pool.append(conn) 496 | 497 | @property 498 | def size(self): 499 | return len(self._pool) 500 | 501 | def get_conn(self): 502 | while 1: 503 | if self._pool: 504 | return self._pool.popleft() 505 | elif self._count < self._maxsize: 506 | self.init_pool() 507 | else: 508 | return self.wait_conn() 509 | 510 | def wait_conn(self): 511 | timer = None 512 | child_gr = greenlet.getcurrent() 513 | main = child_gr.parent 514 | try: 515 | if self._wait_timeout: 516 | timer = Timeout(self._wait_timeout) 517 | timer.start() 518 | self._wait.append(child_gr.switch) 519 | return main.switch() 520 | except TimeoutException: 521 | raise Exception("timeout wait connections, connections size %s", 522 | self.size) 523 | finally: 524 | if timer: 525 | timer.cancel() 526 | 527 | def release(self, conn): 528 | if self._wait: 529 | switch = self._wait.popleft() 530 | self._ioloop.add_callback(switch, conn) 531 | else: 532 | self._pool.append(conn) 533 | 534 | def quit(self): 535 | self._started = False 536 | self._event.set() 537 | 538 | def _close_all(self): 539 | for conn in tuple(self._pool): 540 | conn.close() 541 | self._pool = None 542 | 543 | def start(self): 544 | # self.init_pool() 545 | self._started = True 546 | self._event.wait() 547 | self._close_all() 548 | -------------------------------------------------------------------------------- /greentor/mysql.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from __future__ import absolute_import 4 | import sys 5 | import socket 6 | import errno 7 | import traceback 8 | import time 9 | 10 | from pymysql import err 11 | from pymysql.connections import DEBUG, Connection 12 | 13 | from .green import AsyncSocket, Pool, green 14 | 15 | __all__ = ('patch_pymysql', 'ConnectionPool') 16 | 17 | 18 | def _connect(self, sock=None): 19 | try: 20 | if sock is None: 21 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 22 | sock = AsyncSocket(sock) 23 | sock.set_connecttimeout(self.connect_timeout) 24 | sock.connect((self.host, self.port)) 25 | sock.set_nodelay(True) 26 | self._sock = sock 27 | # self._rfile = _makefile(sock, 'rb') 28 | self._rfile = sock 29 | self._next_seq_id = 0 30 | 31 | self._get_server_information() 32 | self._request_authentication() 33 | 34 | if self.sql_mode is not None: 35 | c = self.cursor() 36 | c.execute("SET sql_mode=%s", (self.sql_mode, )) 37 | 38 | if self.init_command is not None: 39 | c = self.cursor() 40 | c.execute(self.init_command) 41 | c.close() 42 | self.commit() 43 | 44 | if self.autocommit_mode is not None: 45 | self.autocommit(self.autocommit_mode) 46 | except BaseException as e: 47 | self._rfile = None 48 | if sock is not None: 49 | try: 50 | sock.close() 51 | except: 52 | pass 53 | 54 | if isinstance(e, (OSError, IOError, socket.error)): 55 | exc = err.OperationalError( 56 | 2003, "Can't connect to MySQL server on %r (%s)" % (self.host, 57 | e)) 58 | # Keep original exception and traceback to investigate error. 59 | exc.original_exception = e 60 | exc.traceback = traceback.format_exc() 61 | if DEBUG: print(exc.traceback) 62 | raise exc 63 | 64 | # If e is neither DatabaseError or IOError, It's a bug. 65 | # But raising AssertionError hides original error. 66 | # So just reraise it. 67 | raise 68 | 69 | 70 | def _read_bytes(self, num_bytes): 71 | self._sock.set_readtimeout(self._read_timeout) 72 | while True: 73 | try: 74 | data = self._rfile.read(num_bytes) 75 | break 76 | except (IOError, OSError) as e: 77 | if e.errno == errno.EINTR: 78 | continue 79 | raise err.OperationalError( 80 | 2013, 81 | "Lost connection to MySQL server during query (%s)" % (e, )) 82 | if len(data) < num_bytes: 83 | raise err.OperationalError( 84 | 2013, "Lost connection to MySQL server during query") 85 | return data 86 | 87 | 88 | class ConnectionPool(Pool): 89 | def __init__(self, max_size=32, keep_alive=7200, mysql_params={}): 90 | super(ConnectionPool, self).__init__(max_size=max_size, 91 | params=mysql_params) 92 | self._keep_alive = keep_alive # 为避免连接自动断开,配置连接ping周期 93 | 94 | def create_raw_conn(self): 95 | conn = Connection(**self._conn_params) 96 | conn._reconnect_time = self._reconnect_timestamp() 97 | return conn 98 | 99 | def _reconnect_timestamp(self): 100 | return time.time() + self._keep_alive 101 | 102 | def get_conn(self): 103 | conn = super(ConnectionPool, self).get_conn() 104 | if conn._reconnect_time < time.time(): 105 | conn.ping() # 超过重连时间,需要尝试重连一下 106 | return conn 107 | 108 | def release(self, conn): 109 | conn._reconnect_time = self._reconnect_timestamp() 110 | super(ConnectionPool, self).release(conn) 111 | 112 | 113 | def patch_pymysql(): 114 | sys.modules["pymysql"].connections.Connection.connect = _connect 115 | sys.modules["pymysql"].connections.Connection._read_bytes = _read_bytes 116 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | from setuptools import setup, find_packages 3 | setup( 4 | name="greentor", 5 | version="0.1", 6 | packages=find_packages(), 7 | install_requires=['tornado==4.3', 'greenlet', "PyMySQL==0.7.4"], 8 | author="Timmy", 9 | author_email="zhu327@gmail.com", 10 | description="Greenlet support to tornado. inspired by motor and gevent", 11 | license="MIT", 12 | keywords="tornado Greenlet async", 13 | url="https://github.com/zhu327/greentor", ) 14 | -------------------------------------------------------------------------------- /tests/hello.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | from greentor import green 4 | # green.enable_debug() 5 | from greentor import mysql 6 | mysql.patch_pymysql() 7 | 8 | import pymysql 9 | pymysql.install_as_MySQLdb() 10 | 11 | import MySQLdb 12 | 13 | import tornado.httpserver 14 | import tornado.ioloop 15 | import tornado.options 16 | import tornado.web 17 | import tornado.gen 18 | 19 | from tornado.options import define, options 20 | 21 | define("port", default=8000, help="run on the given port", type=int) 22 | 23 | 24 | class MainHandler(tornado.web.RequestHandler): 25 | @green.green 26 | def get(self): 27 | connect = MySQLdb.connect(user='root', 28 | passwd='', 29 | db='test', 30 | host='localhost', 31 | port=3306, 32 | charset='utf8') 33 | cursor = connect.cursor() 34 | cursor.execute('SELECT * FROM app_blog LIMIT 1') 35 | result = cursor.fetchone() 36 | cursor.close() 37 | connect.close() 38 | self.finish(u'

{}

{}

'.format(result[1], result[2])) 39 | 40 | 41 | pool = mysql.ConnectionPool(mysql_params={ 42 | 'user': 'root', 43 | 'passwd': '', 44 | 'db': 'test', 45 | 'host': 'localhost', 46 | 'port': 3306, 47 | 'charset': 'utf8' 48 | }) 49 | 50 | 51 | class ConnectionPoolHandler(tornado.web.RequestHandler): 52 | @green.green 53 | def get(self): 54 | connect = pool.get_conn() 55 | cursor = connect.cursor() 56 | cursor.execute('SELECT * FROM app_blog LIMIT 1') 57 | result = cursor.fetchone() 58 | cursor.close() 59 | pool.release(connect) 60 | self.finish(u'

{}

{}

'.format(result[1], result)) 61 | 62 | 63 | def main(): 64 | tornado.options.parse_command_line() 65 | application = tornado.web.Application([(r"/", MainHandler), 66 | (r"/pool/", ConnectionPoolHandler)], 67 | debug=True) 68 | http_server = tornado.httpserver.HTTPServer(application) 69 | http_server.listen(options.port) 70 | tornado.ioloop.IOLoop.instance().start() 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /tests/requirements.txt: -------------------------------------------------------------------------------- 1 | greenlet==0.4.10 2 | PyMySQL==0.7.4 3 | tornado==4.3 4 | git+https://github.com/zhu327/greentor.git 5 | --------------------------------------------------------------------------------