廖雪峰老师的python3实战部分(个人博客),是基于aiohttp的,之前跟着廖老师写的时候,有许多问题很困惑,最近打算重写一遍。重写之前,发现aiohttp的在线文档中有多个project_demos,遂选择了一个polls练手,重新熟悉一下用法。
另外一个demo_chat涉及websocket编程,以后再做。
另外,数据库选用的是PostgreSQL。
app = web.Application()
app['config'] = get_config(argv)
,get_config() 由 settings.py定义。aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(_templates_path))
app.on_startup.append(init_pg) 和 app.on_cleanup.append(close_pg)
完成。setup_routes(app)
,routes.py定义。routes direct to handlers, 即本项目中的views.py。setup_middlewares(app)
,middlewares.py定义。trafaret
,并可以载入yaml
格式,并能定位真实文件的错误(Trafaret-config is a wrapper that loads yaml and checks config using trafaret while keeping track of actual lines of file where error has happened.) pub_date
用的default=sqlalchemy.sql.func.now()·
。Table choice 设置外键 question_id
,并采用ondelete='CASCADE'
, when PrimaryKey item deleted, ForeignKey item also deleted.engine = await aiopg.sa.create_engine(conf)
,采用sqlalchemy的异步驱动aiopg.sa建立engine,其中conf = app['config']['postgres']
。(app['config'] = get_config(argv)
,在settings.py定义)。 然后获得app['db'] = engine
, 用以后建立连接,即app[‘db’].connect()。await app['db'].wait_closed()
。async def get_question(conn, question_id)
,基于conn 和 question_id
,查询到的记录(question_record, choice_records)。因为基于某个question_id,只查到一个question_text相关的内容,所有是question_record,而对应的choice内容是多条,用choice_records。result = await conn.execute(question.select().where(question.c.id==question_id))
,直接用table_name.select(),然后叠加 .where()
,其中c
是 sqlalchemy的expression language语法,是column的意思。async def vote(conn, question_id, choice_id)
,其中的更新语法类似,但涉及一个returning
用法,即choice.update().returning(*choice.c).where().where().values()
,表示更新后返回的是choice的所有columns;另外可以多重 .where()
限定条件;最后的.values(votes = choice.c.votes+1)
,直接用字段 votes 操作。await
。一旦决定采用异步的方式,都必须保持一致。setup_routes() 建立四类不同的url与相应的handler的映射,方法是:
router.add_route(method, path, handler, *, name=None, expect_handler=None)
而
router.add_get(path, handler, *, name=None, allowe_head=True, **kwargs)
其实是明确了method
的router.add_route()
; router.add_post()
类似。
但注意:router.add_get(path, handler, name=’route’) adds two routes: first for GET with name ‘route’ and second for HEAD with name ‘route-head’.
app.router.add_get('/', index, name='index')
, 索引页即polls的questions列表首页。app.router.add_get('/poll/{question_id}', poll, name='poll')
, 某question_id的poll选项app.router.add_get('/poll/{question_id}/results', results, name='results')
某question_id的poll 结果app.router.add_post('/poll/{question_id}/vote', vote, name='vote')
,无独立页面展示,作为过渡url,从poll/question_id 页面的form提交—-> /poll/question_id/vote —> /poll/question_id/results。setup_static_routes(app)
,语法: add_static(prefix, path, *, name=None, expect_handler=None,….), 本项目为 app.router.add_static('/static/', path=PROJECT_ROOT / 'static', name='static')
。注: add_static() 只用于开发. 生产中,静态文件由web服务器处理(如 nginx or apache.)
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(_templates_path))
定义的loader中的相关模板 *.html 。
@aiohttp_jinja2.template('index.html')
async def index(request):
async with request.app['db'].acquire() as conn: # aiopg.sa.create_engine(URL)
cursor = await conn.execute(db.question.select())
records = await cursor.fetchall()
questions = [dict(row) for row in records]
return {'questions': questions}
注意: 通常的fetchall() 返回a list of tuples,但这里的fetchall(), like aiopg.Cursor.fetchall(), but
。而RowProxy,是 a collections.abc.Mapping for representing a row in query result; Keys are column names, values are result values.
returns a list of RowProxy.
所以上面代码中的records
,就是 a list of RowProxy 或者 a list of collections.abc.Mapping。用dict(row)处理(??),得到 a list of dicts 形如 [{‘id’: xx, ‘question_text’:xx}, {…}, {…}], 最终传给模板变量questions进行渲染。
对应:index.html:
<li><a href="{{ url('poll', question_id=question.id) }}">{{ question.question_text }}a>li>
其中 ‘poll’ 指的是,对应此链接url的路由为routes.py定义的路由‘poll’,变量question_id则不言而喻。
相当于单击polls主页 (/) 的某个question 时,会进入某个question的详情页(/poll/question_id/)。
@aiohttp_jinja2.template('detail.html')
async def poll(request):
async with request.app['db'].acquire() as conn:
question_id = request.match_info['question_id']
try:
question, choices = await db.get_question(conn, question_id) # get_question() defined in db.py
except db.RecordNotFound as e: # class RecordNotFound defined in db.py
raise web.HTTPNotFound(text=str(e))
return {
'question': question,
'choices': choices
}
对应: detail.html
<form action="{{ url('vote', question_id=question.id) }}" method="post">
{% for choice in choices %}
<input type="radio" name="choice" id="choice{{ loop.index }}" value="{{ choice.id }}" />
<label for="choice{{ loop.index }}">{{ choice.choice_text }}label><br/>
{% endfor %}
<input type="submit" value="Vote">
form>
form: action,对应的 method 为 post,对应的url的路由为’vote’。
input:“radio”表示type为单选按钮,提交form时,input 的 name为‘choice’,value为choice.id。
async def vote(request):
async with request.app['db'].acquire() as conn:
question_id = int(request.match_info['question_id']) # why int()
data = await request.post() # see detail.html --> the post of
try:
choice_id = int(data['choice']) # 'choice' is the name of
except (KeyError, TypeError, ValueError) as e:
raise web.HTTPBadRequest(text='You have not specified choice value') from e
try:
await db.vote(conn, question_id, choice_id)
except db.RecordNotFound as e:
raise web.HTTPNotFound(text=str(e))
router = request.app.router # get router info of app
url = router['results'].url_for(question_id=str(question_id))
# router['results']: router.add_get('/poll/{question_id}/results', results, name='results')
return web.HTTPFound(location=url)
最后利用 router[‘results’].url_for() 方法,跳转到 /poll/{question_id}/results页面
@aiohttp_jinja2.template('results.html')
async def results(request):
async with request.app['db'].acquire() as conn:
question_id = request.match_info['question_id']
try:
question, choices = await db.get_question(conn, question_id)
except db.RecordNotFound as e:
raise web.HTTPNotFound(text=str(e))
return {
'question': question,
'choices': choices
}
和 poll(request) 代码相同,只是对应的html模板不同。
对应:results.html
{% block content %}
<ul>
{% for choice in choices %}
<li>{{ choice.choice_text }} -- {{ choice.votes }} vote(s) li>
{% endfor %}
ul>
<a href="{{ url('poll', question_id=question.id) }}">Vote again?a>
{% endblock %}
链接的 url 路由为 ‘poll’, 即点击 ‘Vote again?’, 会跳转到 /poll/question_id 页面。
封装了两个异常处理函数 handle_404(request) 和 handle_500(request)。
注意: create_error_middleware(overrides),其实是个带参数的decorator,并且内部还调用了@web.middleware来修饰 error_middleware(request, handler)。
def create_error_middleware(overrides):
@web.middleware
async def error_middleware(request, handler):
try:
response = await handler(request)
override = overrides.get(response.status)
if override:
return await override(request)
return response
except web.HTTPException as ex:
override = overrides.get(ex.status)
if override:
return await override(request)
raise
return error_middleware
DSN = "postgresql://{user}:{password}@{host}:{port}/{database}"
def setup_db(config):
db_name = config['database']
db_user = config['user']
db_password = config['password']
conn = admin_engine.connect()
conn.execute("DROP DATABASE IF EXISTS %s" % db_name)
# conn.execute("CREATE USER IF NOT EXISTS %s WITH PASSWORD '%s'" % (db_user, db_password))
conn.execute("CREATE DATABASE %s ENCODING 'UTF8'" % db_name)
conn.execute("GRANT ALL PRIVILEGES ON DATABASE %s TO %s" % (db_name,db_user))
conn.close()
def create_tables(engine=test_engine):
meta = MetaData()
meta.create_all(bind=engine, tables=[question, choice])
def drop_tables(engine=test_engine):
meta = MetaData()
meta.drop_all(bind=engine, tables=[question, choice])
demo_images