#tornadopy Web Framework A simple app web framework based on tornado.
###version:1.0.0
tornadopy 是基于Tornado的web mvc框架。tornadopy 大量参考和借鉴了Django的设计模式,形成一套基于tornado的Django like应用层开发框架。tornado 建议使用4.0以上版本,注意:tornadopy不支持基于WSGI的应用。
##框架依赖
- future
- tornado>=3.2
##安装
- pip:
pip install tornadopy
##快速入门
-
导引:
-
####目录结构示例:
|- app +- myapp1 | |- urls.py | |- handlers | |- hello_china.py | |- hello_usa.py | |- ... | +- myapp2 | |- ... | +- logs | +- middlewares | |- ... | +- settings | |- settings.py | |- settings_online.py | +- httpmodules | |- ... | +- templates | |- ... | +- statics | |- ... +- runserver.py
-
project_web :
你可以参考project目录下的 `project_web` 项目示例,并运行: python demo/runserver.py --address=0.0.0.0 --port=8000 --settings=settings.setting 启动服务后,在浏览器中查看 `127.0.0.1:8000` ,你应该可以看到一个欢迎页面!
-
-
####settings配置:
在项目runserver.py中,你可以指定使用的预设值的默认配置文件,如配置文件在应用根目录app.settings下的setting.py:
os.environ.setdefault('tornadopy_APP_SETTINGS', 'settings.setting')
在程序启动时,首先会检查
--settings=
是否给定如--settings=settings.setting
,框架将自动加载应用目录下settings模块中的setting.py配置文件,如果未规定,则使用以上预设值默认配置。在项目中使用配置:from tornadopy import settiings project_path = settings.PROJECT_PATH
DEBUG
: 控制应用是否处于debug状态,等同于 tornado的debug参数。
XHEADERS
: 控制是否开启httpserver的 xheaders。你不用再额外传递一个xheader参数。 -
####webserver:
最简单的server启动方式:
from tornadopy.webserver import run run()
这将启动一个默认的tornado进程,如果想控制更多细节,你可以:
from tornadopy.webserver import Server from tornado.options import define define("your_arg", default='default', help='your command args', type=str) serv = Server() serv.parse_command() #你必须调用parse_command()来处理tornado options,否则无法加载配置 serv.load_urls() serv.load_application(application=None) # 或根据你的需求自定义Application serv.server_start(sockets=None, **kwargs)
很多情况下,我们希望对Application对象进行个性化的处理,通常,我们习惯于实现一个Application子类:
from tornadopy.application import Application class MyApp(Application): """your custom code""" Server().load_application(MyApp) ...
load_application
接收一个tornadopy.applicatoin.Application
类实例或其子类如果要使用tornadopy的全部功能,你的自定义
application
不能直接继承于tornado.web.Application
。Server对象的
server_start
,load_all
以及tornadopy.run
支持自定义的httpserver
参数Server().server_start(no_keep_alive=True,backlog=512)
server.load_all
:如果你希望控制一些细节,但是又希望更简单的启动服务 ,load_all()
会是更好的选择。serv = Server() serv.load_all(application=MyApp,no_keep_alive=True,backlog=512) serv.start()
与
server_start
不同的是,load_all,以及tornadopy.run
会帮你自动parse_command
,处理tornado.options ,所以不需要手动调用它。其他方式你需要手动调用parse_command
。parse_command
: Server对象的parse_command
方法的作用等同于 tornado 中的parse_command_line
同时为你初始化 log日志配置,你必须放在define
语句之后调用,否则定义的参数将不起作用。你可能注意到
start
方法,这个方法类似于ioloop.start ,同时在debug模式下打印一些基础启动信息。server_start
还会帮你处理httpserver的加载。所以,当你使用server_start时,不需要在调用start
方法或ioloop.start()
。当你使用
load_all
来载入application
和httpserver
时,使用start
是更好的选择 。 -
####app:
如顶述目录结构,您可以在应用根目录下创建多个app模块,假如你建立了应用myapp1,应用目录中必须包含
urls.py
。 同时,要加载你的应用,你需要在配置文件中INSTALLED_APPS
元组中增加你的app配置,如你的应用根目录app下存在myapp1,myapp2,则需:INSTALLED_APPS = ( 'myapp1', 'myapp2', )
系统启动后,将自动加载myapp1,myapp2中的urls.py下的路由配置。
-
####urls:
每一个app都必须包含一个urls.py的路由表文件。你可以像这样配置路由:
from tornadopy import Url, route u = Url('myapp1.handlers',abc='abc') urls = route( u(name='Index', pattern=r'/?', handler='main_handler.Main'), u(name='Login', pattern=r'/login/?', handler='main_handler.Login'), u(name='User', pattern=r'/user/?', handler='main_handler.User'), )
Url()接受参数
prefix
模块前缀,上述例子中最终的加载handler模块为:myapp1.handlers.main_handler.Main
. Url()还可以接受若干全局预加载参数。其作用等同于tornado 路由的kwargs参数,此参数将会被解析到每个handler的_url_kwargs变量中,如上述abc='abc'。main_handler.Main:
from tornadopy.handler import WebHandler class Main(WebHandler): def get(self): self.finish(self._url_kwargs['abc']) # print abc
每一个url还可以接受一个kwargs参数,这个参数仅对当前路由有效,其作用等同于上述的_url_kwargs,同样会被解析到_url_kwargs字典中。如果遇到同名参数,路由的kwargs会覆盖Url()中定义的同名参数,但仅影响当前路由。
u(name='User', pattern=r'/user/?', handler='main_handler.User',kwargs={'abc':'other'}),
你也可以直接import对应的handler:
from myapp1.handlers.main_handler import Main u = Url() urls = route( u(name='Index', pattern=r'/?', handler=Main), )
路由分组: 当你有许多路由时,为了便于管理,你可以将路由分散到多个文件中:
url_photo.py: url=Url('myapp1.handlers') URLS = route( url(r'photo/upload/?','photo.UploadHandler'), ... ) url_blog.py: url=Url('myapp1.handlers') URLS = route( url(r'blog/list/\d*/?','blog.ListHandler'), ..., ) urls.py: from tornadopy import route from myapp1.url_blog import URLS as blog_urls url = Url('myapp1.handlers') URLS = route( include('/','myapp1.url_photo.URLS'), include('/',blog_urls), url(r'/user/me/?','user.MeHandler'), )
-
####log: tornadopy支持使用原生tornado日志模块,或tornadopy基于logging扩展日志,tornadopy默认启用扩展日志代替tornado默认的log,除非
--disable_log=False
,则启用tornado.log模块 。from tornadopy.logger import syslogger syslogger.error("this is a error.")
日志配置中的默认日志HANDLER默认为
tornadopy.logger.ProcessLogTimedFileHandler
,此handler支持在多进程状态下安全的写入日志文件,如需日志文件名按照端口号区分,通过设置--log_port_prefix=true
,--logging_dir
如果设定, 配置文件中的LOGGING_DIR
将被忽略。你可以自定义你的日志配置,或新增日志,你只需要在LOGGING配置中加入类似段落:
{ 'name': 'tornado', 'level': 'INFO', 'log_to_stderr': False, 'when': 'midnight', 'interval': 1, 'filename': 'tornado.log' },
注:请保留默认的几个日志配置。
-
####模板引擎:
在配置文件
TEMPLATE_CONFIG
中配置模板。 其中,template_engine
决定使用什么模板加载器(Loader) ,默认为 None,使用自带的模板引擎。 可选择使用 mako 或 jinja2 ,需要安装依赖库 mako或jinja2 。TEMPLATE_CONFIG = { 'template_engine': 'tornadopy.template.mako_loader.MakoTemplateLoader' #使用mako模板 'filesystem_checks': True, # 通用选项 'cache_directory': '../_tmpl_cache', #通用选项,模版编译临时文件目录 'collection_size': 50, #mako选项,暂存入内存的模版项,可以提高性能,详情见mako文档 'cache_size': 0, #jinja选项,类似于mako的collection_size,设定为-1为不清理缓存,0则每次都会重编译模板 'format_exceptions': False, #mako选项,格式化异常输出 'autoescape': False #jinja2选项,默认转义设定 # 支持mako或jinja其他配置参数,详见相关文档。 }
-
####handler:
如果需要使用tornadopy提供的功能,业务handler需要继承自
tornadopy.handler.WebHandler
或tornadopy.handler.ApiHandler
.WebHandler提供了兼容mako,jinja2模板引擎加载,中间件hook的功能。并额外提供两个方法
on_prepare
和complete_finish
, 这两个方法分别在prepare
和on_finish
方法执行结束后调用。同时,tornadopy提供了一个
FlashMessageMixIn
,可以配合handler实现消息闪现的功能,其作用类似flask中的flash。你的handler需要继承FlashMessageMixIn
:class MyHandler(FlashMessageMixIn,WebHandler): def get(self): self.flash("Welcome back, %s" % username, 'success') base.html ------------ {% set messages = handler.get_flashed_messages() %} {% if messages %} <div id="flashed"> {% for category, msg in messages %} <span class="flash-{{ category }}">{{ msg }}</span> {% end %} </div> {% end %}
-
####中间件:
tornadopy实现了简单的中间件功能,其行为和功能类似于 Django 的中间件。在这里引用一张Django中间件的流程图:
我们可以参考这张图来理解tornadopy的中间件设计。tornadopy中间件遵循了类似于Django中间件的设计,但因为框架本身的差异性,tornadopy中间件 采用对RequestHandler执行流程中插入hook的方式,中间件默认没有返回值,你可以在需要的请求阶段对请求对象(request或handler)进行处理。
*注:要使用中间件功能,你的handler必须继承自
tornadopy.handler.WebHandler
或tornadopy.handler.ApiHandler
自定义中间件:
class MyMiddleware(object): def process_init(self, application): """ :param application: 应用程序对象,此方法在tornado启动时执行一次 """ def process_call(self, request, clear): """ 在请求request对象创建时,参数为请求对象,此时还未匹配路由handler :param request: 请求对象 """ def process_request(self, handler, clear): """ 匹配路由后,执行处理handler时调用 :param handler: handler对象 支持异步 """ def process_render(self, handler, clear, template_name, **kwargs): """ 此方法在调用render/render_string时发生 :param handler: handler对象 :param template_name: 模板名称 :param kwargs: 模板参数 """ def process_response(self, handler, clear, chunk): """ 请求结束后响应时调用,此方法在render之后,finish之前执行,可以对chunk做最后的封装和处理 :param handler: handler对象 :param chunk : 响应内容,chunk为携带响内容的list,你不可以直接对chunk赋值, 可以通过chunk[index]来改写响应内容,或再次执行handler.write() """ def process_endcall(self, handler, clear): """ 请求结束后调用,此时已完成响应并呈现用户,一般用来处理收尾操作,清理缓存对象,断开连接等 :param handler: handler对象 """ def process_exception(self,handler, clear, typ, value, tb): """ 请求过程引发异常时调用,你可以通过这个方法捕获在请求过程中的未捕获异常 如果没有中间件实现此方法,则调用tornado RequestHandler.log_exception方法。 :param handler: handler对象 :param typ: 等同RequestHandler.log_exception的参数,异常类型值和异常堆栈信息 :param value:异常值信息 :param tb:异常堆栈 """
编写中间件需实现其中任何一个方法即可,中间件的执行流程中在请求阶段,
call
,request
按照中间件的声明顺序执行, 在响应过程中,exception
,response
,endcall
和render
则是按声明顺序倒序执行) , 在tornado 4.0 中,process_request
支持异步调用。在中间件中返回任意真值,如 return 1,则停止当前请求的其余中间件相同方法的执行,进入下一个中间件流程。特别是当finish请求后,需return 1,例如:
def process_request(self, handler, clear): if not handler.user: handler.finish('auth failure.') return 1
调用finish后必须返回真值,
return 1
,这样会跳过剩余中间件的process_request
方法,直接进入finish
阶段 ,否则将继续执行 其余中间件process_request
方法,以及get/post
方法。clear: 如果希望当前请求剩余的所有中间件流程完全终止,则在方法头部调用此方法,以清空中间件的执行队列 与return 1不同,clear() 会导致其余的中间件全部方法在该请求中失效,而return 1只会导致所有中间件的当前流程方法失效。
默认提供的中间件:
tornadopy.middleware.accesslog.AccessLogMiddleware
:定制处理日志打印tornadopy.middleware.dbalchemy.DBAlchemyMiddleware
:使用SqlAlchemy需要引入该中间件tornadopy.middleware.session.SessionMiddleware
:提供session功能tornadopy.middleware.session.SignalMiddleware
:事件信号通知中间件,对django的signal封装
中间件配置:
MIDDLEWARE_CLASSES
:实现中间件需要在配置文件中加入。其顺序决定中间件请求处理的顺序。 例如:MIDDLEWARE_CLASSES = ( # access_log中间件放在最上面,因为在响应阶段,最上边的中间件处理方法process_endcall会在最后执行。 'tornadopy.middleware.accesslog.AccessLogMiddleware', 'tornadopy.middleware.session.SessionMiddleware', 'tornadopy.httpmodule.httpmodule.HttpModuleMiddleware', )
-
####HTTP处理器:
中间件提供了对请求处理流程的干预能力,使得我们可以控制请求过程中的各个方面。但是我们无法从容的对特定请求的特定过程进行干预。HTTP处理器提供了路由级别的请求处理能力。HTTP处理器分为两种:
全局处理器
和路由处理器
。你需要在中间件中加入
tornadopy.httpmodule.httpmodule.HttpModuleMiddleware
启用路由处理功能。自定义HTTP处理器模块需要继承自
tornadopy.httpmodule.BaseHttpModule
,并实现begin_request
,begin_render
,begin_response
,complete_response
中任意一个方法即可,方法行为和功能类似中间件,同样,begin_request
方法在tornado4.0以上版本支持异步调用。HTTP处理器配置:
-
全局处理器:通用路由处理器会处理所有请求的对应过程。行为等同于中间件,但是不同的是,在响应阶段,处理方法不同于中间件是倒序执行,而是顺序执行,同时需要注意的是,全局处理器总是优先于路由处理器之前执行。
COMMON_MODULES = ( 'httpmodule.auth.AuthModule', 'httpmodule.ipauth.ipblack', )
-
路由处理器:根据配置在具体的路由请求中使用,格式为
'正则path或路由name':['module1','!module2',]
。ROUTE_MODULES = { '^/user/.*$':['httpmodule.loginmodule','!httpmodule.ipauth.ipblack',], 'Index': ['!httpmodule.auth.AuthModule',] }
路由处理器为一个字典,字典的键为需要匹配的路由的名称(定义urls时指定),或路由path的正则表达式,如上例。 值为处理该路由请求的模块。 在请求过程中,满足匹配条件的路由处理器将被在指定过程触发,根据模块实现的方法来进行相应的处理。 如果在配置模块的前面添加了
!
符号,将反选在COMMON_MODULES
中同名的模块,那么在请求过程中,全局处理器将不再匹配的路由中执行。-
当你需要对某个路由禁用全部的
COMMON_MODULES
模块处理器时,你不需要写很多!,你只需要配置一条!all
即可:ROUTE_MODULES = { '^/user/.*$':['httpmodule.loginmodule','!all',], }
!all
将会禁用COMMON_MODULES
定义的全部处理器。由于COMMON_MODULES
总是优先于路由处理器顺序执行,通过!all
可以让你在ROUTE_MODULES
中个性化你的路由处理器执行顺序,而其他的路由处理器配置不受影响。注: 在路由处理器配置中添加
!
符号的处理器,必须存在于COMMON_MODULES
中,否则将引起异常。
如上实例中,请求名为Index 的路由将不执行
COMMON_MODULES
中配置的httpmodule.auth.AuthModule
模块,请求满足^/user/.*$
正则的路由不执行httpmodule.ipauth.ipblack
模块。 -
-
-
####缓存:
tornadopy支持使用memcache,redis,file,localcache作为缓存。缓存模块主要抽取自django.cache,相关配置可参考 Django文档,redis是新增支持。
-
redis缓存模块配置,例如:
'rediscache': { 'BACKEND': 'tornadopy.cache.backends.rediscache.RedisCache', 'LOCATION': '127.0.0.1:6379', 'OPTIONS': { 'DB': 0, 'PARSER_CLASS': 'redis.connection.DefaultParser', 'POOL_KWARGS': { 'socket_timeout': 2, 'socket_connect_timeout': 2 }, 'PING_INTERVAL': 120 # 定时ping redis连接池,防止被服务端断开连接(s秒) } },
redis配置中,BACKEND 支持两种:
tornadopy.cache.backends.rediscache.RedisCache
和tornadopy.cache.backends.rediscache.RedisClient
。RedisClient
提供一个原生的client属性,提供基础的,原生的redis-py功能,而RedisCache
继承自RedisClient
是提供高层缓存使用,其实现了和tornadopy.cache.backends.memcached.MemcachedCache
相同的接口,且行为和功能一致。如果你需要一些高级的redis方法,请使用RedisClient,如果仅仅需要基础的缓存功能,使用RedisCache即可。RedisCache同样提供client属性。
-
-
####DB&ORM:
tornadopy提供了对SqlAlchemy ORM的支持,模块
tornadopy.db.dbalchemy
对sqlalchemy进行了基本的封装使其更加易用。同时,tornadopy提供了一个简单轻量级的db模块basedb,此模块来自与web.py框架的db模块,基本的使用方式可以参考web.py cookbook,下面主要介绍dbalchemy模块。数据库配置:
DATABASE_CONNECTION = { 'default': { 'connections': [{ 'ROLE': 'master',#主库 'DRIVER': 'mysql+mysqldb', 'UID': 'root', 'PASSWD': '', 'HOST': '', 'PORT': 3306, 'DATABASE': '', 'QUERY': {"charset": "utf8"} }, { 'ROLE': 'slave',#从库,可配置多个 'DRIVER': 'mysql+mysqldb', 'UID': 'root', 'PASSWD': '', 'HOST': '', 'PORT': 3306, 'DATABASE': '', 'QUERY': {"charset": "utf8"} }] } }
PING_DB
:定时对数据库进行ping操作,保持连接池可用。默认:500ssqlalchemy配置,列出部分,可自行参考sqlalchemy文档增加配置项,该配置项对所有连接全局共享。
SQLALCHEMY_CONFIGURATION = { 'sqlalchemy.connect_args': { 'connect_timeout': 3 }, 'sqlalchemy.echo': False, #开启打印sql日志 'sqlalchemy.max_overflow': 10, 'sqlalchemy.echo_pool': False, 'sqlalchemy.pool_timeout': 5, 'sqlalchemy.encoding': 'utf-8', 'sqlalchemy.pool_size': 100, 'sqlalchemy.pool_recycle': 3600, 'sqlalchemy.poolclass': 'QueuePool' # 手动指定连接池类 }
使用方式:
from tornadopy.db.dbalchemy import Model from sqlalchemy import * from sqlalchemy.dialects.mysql import INTEGER class BaseModel(Model): __abstract__ = True __connection_name__ = 'default' #对应配置中的default项 ID = Column('id', INTEGER(11, unsigned=True), primary_key=True, nullable=False) # primary key class User(BaseModel): __tablename__ = 'users' name = Column(String(10), nullable=False) password = Column(String(64), nullable=False)
- 查询:如上,Model对象的
Q
属性提供一个默认简单的查询对象,你可以像这样User.Q.filter(User.name='jack').all()
。 也可以像这样:User.session.query(User)
使用session属性创建查询对象。
当存在主从数据库时,执行增删改操作需要指明使用主库的会话对象:
session = User.session.using_master()
-
分页:
query = User.Q.all() pagelist_obj = query.paginate(page=1, per_page=10, default=None) total = pagelist_obj.total page = pagelist_obj.page items = pagelist_obj.items
使用Sqlalchemy必须在配置中间件中加入:tornadopy.middleware.dbalchemy.DBAlchemyMiddleware
- 查询:如上,Model对象的
-
####异步线程池
tornado本身是异步单线程单进程框架,这样当遇到使用mysql的慢查询时,就会阻塞进程。tornadopy提供一个简单的方式来用线程池包装同步方法。通过配置文件设定
THREADS_NUM
数量来决定线程池的最大线程数,默认为cpu_count。注:新版tornado中内置了
concurrent.run_on_executor
装饰器,可提供同样的功能,tornadopy提供tornadopy.decorators.async_execute
来方便使用线程池来异步化你的同步方法。from tornadopy.decorators.async_execute import async_execute class Test(Base): @coroutine def get(self): a='' b='' #支持使用gen的task模块来同步化异步调用 result = yield self.dosomething(a, b) self.finish(result) @async_execute def dosomething(self,a,b): # 这里可能耗时很久 # something... result='return' return result
-
####session:
tornadopy提供一个简单的session功能,session可以使用tornadopy.cache下的缓存模块或实现了
tornadopy.cache.backends.base.BaseCache
的模块类作为session_store,比如你可以使用memcache、redis或LocalCache缓存来作为session的存储。首先,你需要增加中间件
tornadopy.middleware.session.SessionMiddleware
来开启对session的支持,在具体的路由handler中,可以通过handler.session获取session存储对象。session配置:
SESSION = { 'session_cache_alias': 'default', # 'session_loccache',对应cache配置名 'session_name': '__TORNADOSSID', 'cookie_domain': '', 'cookie_path': '/', 'expires': 0, # 24 * 60 * 60, # 24 hours in seconds,0代表浏览器会话过期 'ignore_change_ip': False, 'httponly': True, 'secure': False, 'secret_key': 'fLjUfxqXtfNoIldA0A0J', 'session_version': 'EtdHjDO1' }
session使用: 你可以像这样读写session:
class LoginHandler(WebHandler): def get(self,uid): self.session['userid'] = uid self.set_expire(3600 * 24 * 30) #30天 class AuthHandler(WebHandler): def get(self,uid): uid = self.session['userid'] class LogoutHandler(WebHandler): def get(self,uid): del self.session['userid']
-
####token:
tornadopy提供一个token功能,token只可以使用tornadopy.cache下的redis缓存模块来实现。
首先,你需要增加中间件
tornadopy.middleware.token.TokenMiddleware
来开启对token的支持,在具体的路由handler中,可以通过handler.token获取token存储对象。注意:其实把token模块做成HTTP处理器更优雅。
token配置:
TOKEN = { 'token_cache_alias': 'default_redis', # 此处必须是 default_redis 'ignore_change_ip': False, 'token_timeout': 2592000, 'secret_key': 'xxxxxxxxxxxxxxxxxxxx', 'token_version': '' }
token使用: 你可以像这样使用token:
class LoginTokenHandler(WebHandler): def get(self): userID = self.get_argument("userID") userName = self.get_argument("userName") tmp_data = { "userID": userID, "userName": userName } tokenID = self.token.save(tmp_data) self.write(tokenID) class LoginTokenTestHandler(WebHandler): def get(self): if self.token.get_current_user() == None: self.write("tokenID 非法") else: self.write("tokenID 是合法的")
-
####signal:
信号组件提供信号的注册与通知处理能力,我们可以在任何地方注册事件,也可以在你希望的任何时候触发事件。在信号触发时,将调用接收信号的callback,进行相关业务逻辑处理。
tornadopy提供了几个与请求过程相关的信号,这几个信号通过信号中间件触发。他们分别是
call_started
,handler_started
,handler_response
,call_finished
,handler_render
,触发事件分别对应中间件process_call
,process_request
,process_response
,process_endcall
,process_render
几个过程 。使用:
from tornadopy.signal import call_started,handler_render class MyService(object): # 通过装饰器来注册事件回调 @receiver(call_started) def callback(**kwargs): """do something...""" def callback(**kwargs): print kwargs # 通过调用信号connect方法注册事件回调 handler_render.connect(callback)
事件回调函数接收一个
**kwargs
参数,在事件触发时,kwargs包含事件的 signal对象,事件源的class,事件参数等。自定义信号事件:
from dispatch import Signal my_event = Signal(providing_args=["args1"]) # 事件触发 my_event.send(sender=somesender.__class__, args1=arg)