python_web

Alembic

安装

pip install alembic

操作步骤

1.初始化仓库

初始化alembic仓库。在终端中,cd到你的项目目录中,然后执行命令alembic init alembic,创建一个名叫alembic的仓库。

2.创建模型(ORM)类:

比如这里创建一个models.py模块,

from sqlalchemy import Column,Integer,String,create_engine,Text
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
    __tablename__ = 'user'
    id = Column(Integer,primary_key=True)
    username = Column(String(20),nullable=False)
    password = Column(String(100),nullable=False)

3.修改配置文件:

  • alembic.ini中设置数据库的连接,sqlalchemy.url = driver://user:pass@localhost/dbname,比如以mysql数据库为例,则配置后的代码为:

    sqlalchemy.url = mysql+mysqldb://root:root@localhost/alembic_demo?charset=utf8
    

    为了使用模型类更新数据库,需要在env.py文件中设置target_metadata,默认为target_metadata=None。使用sys模块把当前项目的路径导入到path中:

    import os
    import sys
    sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/../")
    from models import Base
    ... #省略代码
    target_metadata = Base.metadata # 设置创建模型的元类
    ... #省略代码
    
cd optima_models
#1.初始化,创建一个仓库
	alembic init alembic(anyname)
# 2.修改配置alembic.ini
	sqlalchemy.url = mysql+pymysql://root:123@localhost/optima?charset=utf8
# 3.创建model 
# 4.修改env.py
import os, sys 
sys.path.append(os.path.dirname(os.path.abspath(__file__)) +"/../")
# target_metadata = base_model.Base.metadata
target_metadata = db.Model.metadata   # flask-sqlalchemy

# 5.如果只创建表,不删除表,改成
# existing_db.py
from sqlalchemy import create_engine
from sqlalchemy.schema import MetaData
engine =create_engine('mysql+pymysql://root:123@127.0.0.1:3306/test_tcloud?charset=utf8')
metadata = MetaData(engine)
metadata.reflect()
# env.py 需要引入这个metadata
import existing_db  # 只能新建表,不能修改
target_metadata = existing_db.metadata, db.Model.metadata

4.生成迁移文件

使用命令alembic revision --autogenerate -m "message"可以将当前模型中的状态生成迁移文件。

5.更新数据库

使用alembic upgrade head将刚刚生成的迁移文件,真正映射到数据库中。同理,如果要降级,那么使用alembic downgrade head

6.重复

如果以后修改了代码,则重复4~5的步骤。

生成sql脚本

rm -f alembic/versions/*.py
mysql -uroot -p123 -e "use test_tcloud;truncate table alembic_version"
alembic revision --autogenerate -m "auto generate sql"
alembic upgrade head --sql | tee  migration.sql

if [ x$1 = xc ];then
     # tables need to be created in the database
     alembic upgrade head
fi

部署 uwsgi

uwsgi.ini

[uwsgi]
socket = 127.0.0.1:9000   # 监听到网络套接字 也可以是uwsgi.sock
pythonpath=/PyPro/optima_order
py-autoreload = 1  # 代码修改自动重载
module=server
callable=app
master = true         
processes=4
threads=2
pidfile=uwsgi.pid
daemonize=uwsgi.log
uwsgi --ini uwsgi.int
uwsgi --reload uwsgi.pid 

/etc/nginx/conf.d/default.conf

location / {
	include uwsgi_params;
	uwsgi_pass 127.0.0.1:9000;
}

location /v1/truck {
    include uwsgi_params;
    uwsgi_pass unix:/var/www/trucks/backend/current/tmp/uwsgi.sock;
}

常用的sqlalchemy查询

联合索引

__table_args__ = (
    db.Index("ix_regular_line_truck_location_code_x_y", 'location_code_x', 'location_code_y'),
    db.Index("ix_regular_line_truck_location_name", 'location_name_x', 'location_name_y')
)
modified_time = db.Column(db.TIMESTAMP,
                              nullable=False,
                              default=datetime.now,
                              onupdate=datetime.now)

server_default='北京' # 数据库default字段能看到
# 布尔值类型指定server_default时,需要用text(“0”)或者text(“1”)
# mysql 不支持时间字段默认

Boolean:布尔类型,映射到数据库中的是tinyint类型。

flask-sqlalchemy 会在每个请求上下文内自动创建销毁 session,如果你需要在当前请求内继续使用 session,必须执行 session.rollback();如果确定不再需要这个 session 执行任何数据库操作了,可以不用 rollback,让它在请求结束时自动销毁。

like查询

db.session.query(tablename).filter(tablename.colname.like(''%xxx%))

打印SQL

query = session.query(Model).filter(*filter)
print(str(query))

inner join 且 like

db.session.query(tableA, tableB).join(tableB, tableA.id==tableB.id).filter(tableA.colname.like('%xx%'))
select * from tableA inner join tableB on tableA.id=tableB.id where tableA.colname like '%xx%'

left join

db.session.query(tableA, tableB).outerjoin(tableB,tableA.id==tableB.id).filter(tableA.colname=='xxx')
select * from tableA left outer join tableB on tableA.id=tableB.id where tableA.colname='xxx'

join有多个条件

from sqlalchemy import and_
db.session.query(tableA, tableB).join(tableB, and_(tableA.id==tableB.id,  tableA.col2==tableB.col2)).filter(tableA.colname=='xxx')
select * from tableA left outer join tableB on tableA.id=tableB.id and tableA.col2=tableB.col2 where tableA.colname='xxx'

union

# 组合
q1 = session.query(Users.name).filter(Users.id > 2)
q2 = session.query(Favor.caption.lable("name")).filter(Favor.nid < 2)
ret = q1.union(q2).all() #union默认会去重
ret2 = q1.union_all(q2).all() # union_all不去重
func.COUNT(func.IF((TransactionMessage.tm_read==0 and TransactionMessage.tm_type==1),1,0)).label('t_message_count'),

子查询

subquery = PipelineHistory.query.with_entities(func.max(PipelineHistory.id).label("history_id"),PipelineHistory.pipeline_id,PipelineHistory.result).group_by(
        PipelineHistory.pipeline_id).subquery()

query = Pipeline.query.with_entities(Pipeline.id, Pipeline.name,subquery.c.result).filter(*filter_query).filter(subquery.c.pipeline_id == Pipeline.id)

print(query.all())

between 查询

from sqlalchemy import between
db.session.query(tableA).filter(between(tableA.colA, 'xxx1', 'xxx2'))
select * from tableA where colA between 'xxx1' and 'xxx2'

查找指定月份

from sqlalchemy import func, extract
.filter(extract('month', table.run_time) == 10)
.filter(func.year())

and查询

from sqlalchemy import and_
db.session.query(tableA).filter(and_(tableA.col1='xxx1', tableA.col2='xxx2'))
select * from tableA where (tableA.col1='xxx1' and tableA.col2='xxx2')

or查询

from sqlalchemy import or_
db.session.query(tableA).filter(or_(tableA.col1='xxx1', tableA.col2='xxx2'))
select * from tableA where (tableA.col1='xxx1' or tableA.col2='xxx2')

in 查询

db.session.query(tableA).filter(tableA.col1.in_(['xx1', 'xx2']))
select * from tableA where col1 in ('xx1', 'xx2')

is not NULL

db.session.query(tableA).filter(tableA.col1.isnot(None))
db.session.query(tableA).filter(tableA.col1 != None)
select * from tableA where col1 is not null

distinct

db.session.query(tableA).distinct(tableA.col1).all()

查询指定字段

# 1.
db.session.query(tableA.col1, tableA.col2).all()
# 2.with_entities 返回特定的列去重
tp.query.with_entities(tp.fk_driver_id).distinct().all()
# 3. add_columns (返回 (<Requirement 23>, 23, 'SWCS插件自动化'), ret.Requirement, ret.id, ret.title)
Req.query.add_columns(Req.id,Req.title.label('title')).all()

分页

# 1.paginate
tl_objs = tl.query.filter(*filter_list).order_by(*order_by).paginate(page_index, page_size, False)
current_page = tl_objs.page
total = tl_objs.total
total_pages = tl_objs.pages
# 属性'has_next', 'has_prev', 'items', 'iter_pages', 'next', 'next_num', 'page', 'pages', 'per_page', 'prev', 'prev_num', 'query', 'total
data = tl_objs.items

# 2. limit
count = query.count()
if page_index and page_size:
   query = query.limit(page_size).offset((page_index - 1) * page_size)
data = query.all()

text

tpf.query.filter(text(
        "fk_trans_number='%s' and status!=302 and status!=301 and status!=201 and content is null" %
     trans_number)).count()

tp.query.filter(text("fk_to_location_code=:id and fk_driver_id=:id").params(id=1)).all()

a['296'] = [<296>, b['297']]
b['297'] = [<297>, ...]

执行SQL语句

    tp.query.from_statement(text("select * from Transport_Protocol where fk_to_location_code=1")).all()

假如字符串str 在由N 子链组成的字符串列表strlist 中,则返回值的范围在 1 到 N 之间。

func.find_in_set(business, Location.business)  

查询优化

1. 选择合适的加载方式:

  • 延迟加载(Lazy Loading): 默认情况下,SQLAlchemy 使用延迟加载,只在需要时才加载关联数据。这可以避免不必要的查询,但如果在循环中访问关联属性,可能会导致 N+1 查询问题。
  • 立即加载(Eager Loading): 可以使用 joinedload, subqueryload, selectinload 等选项来指定立即加载关联数据,避免 N+1 查询。选择哪种方式取决于具体场景和查询的数据量。
  • 选择性加载(Select Only): 使用 with_entitiesload_only 方法可以选择只加载需要的字段,减少数据传输量。
# 延迟加载,可能导致 N+1 查询
for user in session.query(User).all():
    print(user.orders)  # 每次循环都会查询 orders 表

# 立即加载,避免 N+1 查询
from sqlalchemy.orm import joinedload
for user in session.query(User).options(joinedload(User.orders)).all():
    print(user.orders)  # 只查询一次 orders 表

# 选择性加载,只加载需要的字段
for user in session.query(User).with_entities(User.id, User.name).all():
    print(user.id, user.name)  # 只查询 id 和 name 字段

2. 使用编译缓存:

  • SQLAlchemy 可以缓存编译后的 SQL 语句,避免重复编译,提高查询效率。可以通过配置 SQLALCHEMY_CACHE_TYPE 来启用缓存。

3. 使用原生 SQL:

  • 对于复杂的查询,直接使用原生 SQL 可能比使用 ORM 更高效。SQLAlchemy 提供了 textexecute 方法来执行原生 SQL 语句。

4. 索引优化:

  • 和直接使用 SQL 一样,确保在数据库中为经常查询的字段创建索引。可以使用 SQLAlchemy 的 Index 对象来创建索引。

5. 数据库优化:

  • 使用数据库连接池、数据库缓存、定期进行数据库维护等措施,同样适用于 SQLAlchemy。

6. 其他优化技巧:

  • 使用 in 操作符进行批量查询: 避免循环中执行多次查询。
  • 使用 bulk_update_mappings 方法进行批量更新: 避免循环中逐条更新数据。
  • 使用 bulk_insert_mappings 方法进行批量插入: 避免循环中逐条插入数据。

https://blog.csdn.net/five3/article/details/70786741

https://www.cnblogs.com/shangwei/p/15654553.html

最慢的就是 add 方式

次慢 bulk_save_objects 方式

慢的 bulk_insert_mappings 方式

快的 engine.execute 方式

最快的 copy_from 方式

如果 使用 bulk_insert_mappings 插入10W条数据需要10秒,那么使用 copy_from 方式只需要1秒

数据库锁

MySQL 默认的锁机制主要是在后台自动管理的,比如在插入、更新和删除时会自动加锁。然而,这些自动锁定通常不包括读取操作。

SELECT * FROM products WHERE id=’3’ FOR UPDATE;

表级: MyISAM
行级: INNODB
由于InnoDB预设是Row-Level Lock,所以只有「明确」的指定主键,
MySQL才会执行Row lock , 上句id是主键,

with_lockmode --> sqlalchemy 0.9.0以后废弃
对需要经常并发操作的表,sqlalchemy可以加锁保证数据一致性,
mode参数
None
update
update_nowait Oracle, PostgreSQL
read

with_for_update(self, read=False, nowait=False, of=None)
db.session.query(User).with_for_update(nowait=True, of=User)

​ mysql 不支持nowait等参数

flask

https://www.cnblogs.com/wupeiqi/articles/7552008.html

flask组件网

配置文件

路由系统

模板语言

请求&响应相关

session & cookie

闪现

蓝图: bluegraphic

请求扩展

中间件

wsgi:

#werkzeug
from werkzeug.wrappers import Request, Response

@Request.application
def hello(request):
    return Response("hello world!")

if __name__ == "__main__":
    from werkzeug.serving import run_simple
    run_simple("localhost", 4000, hello)

#wsgiref
from wsgiref.simple_server import make_server

def runserver(environ, start_response)
    start_response("200 OK", [("Content-Type", "text/html")])
    return [bytes('<h1>Hello web!</h1>', encoding='utf-8'),]

if __name__ == "__main__":
    httpd = make_server('', 8000, runserver)
    httpd.server_forever()

本质是socket

import socket
def handle_request(client):
	buf = client.recv(1024)
	client.send("HTTP/1.1 200 OK\r\n\r\n")
	client.send("Hello, Seven")

def main():
	sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	sock.bind('localhost', 8000)
	sock.listen(5)

	while True:
		connection, address = sock.accpet()
		handle_request(connection)
		connection.close()

if __name__ == "__main__":
	main()

url_for('login') // 反向生成url
@app.route("/login", methods=['GET', 'POST'], endpoint='login')

模板路径:
Flask() 参数中有个template_folder="templates"

return render_template("login.html") # from flask import render_template

2.GET请求:

from flask import request

if request.method == "GET":
    ...

#请求的数据
user = request.form.get("user")

重定向:

from flask import redirect

return redirect("http://www.baidu.com")

@app.route("/index", redirect_to="/index2")

render_template("login.html", **content) # 传入一个字典但是要**,也就是传入# #kwargs
render_template("login.html", error="username is wrong")


HTML:

{% for k, v in user_dict.items() %}
	{{k}}
	{{v.name}}	 {{v['name']}} {{v.get('name')}}
{% endfor %}

自动重启:
app.debug = True

<a href="/detail/{{k}}" > 查看详情</a>

@app.route("/detail/int:nid" methods=['GET'])

session:

from flask import session
	user = session.get("user_info")
	if not user:

url别名, 反向生成: # endpoint

from flask import url_for
	@app.route('/login', methods=[], endpoint='l1')
url = url_for('l1')  # 也能传参
return redirect(url)

配置文件

app:
default_config = ImmutableDict
import collections
d1={}
d1=collections.OrderedDict() #将普通字典转换为有序字典

# app.py
1. app.debug = True  // 少数可以
2. app.config['debug'] = True

3. settings.py
DEBUG = True
app.config.from_pyfile("settings.py")
4. app.config.from_object()

settings.py

class Config(object):
	DEBUG = False
	TESTING = False
	DATABASE_URI = 'sqlite://:memory:'

class ProductionConfig(Config):
	DATABASE_URI = 'mysql://user@localhost/foo'

class DevelopmentConfig(Config):
	DEBUG = True

class TestingConfig(Config):
	TESTING = True

开发环境与正式环境不一样

app.config.from_object('pro_flask.settings.TestingConfig')
# 原理
"os.path.realpath"
module_name, obj_name = import_name.rsplit('.', 1)
module = __import__(module_name, None, None, [obj_name])
return getattr(module, obj_name)
module = <module 'posixpath' from '/usr/lib/python3.5/posixpath.py'>

flask下载文件---文件流

html:

<a name="downloadbtn" class="btn btn-success pull-right" href="/downloadfile/?filename=/root/allfile/123.txt">下载</a>

py:

@app.route('/downloadfile/', methods=['GET', 'POST'])
def downloadfile():
    if request.method == 'GET':
        fullfilename = request.args.get('filename')
        # fullfilename = '/root/allfile/123.txt'
        fullfilenamelist = fullfilename.split('/')
        filename = fullfilenamelist[-1]
        filepath = fullfilename.replace('/%s'%filename, '')
        #普通下载
        # response = make_response(send_from_directory(filepath, filename, as_attachment=True))
        # response.headers["Content-Disposition"] = "attachment; filename={}".format(filepath.encode().decode('latin-1'))
        #return send_from_directory(filepath, filename, as_attachment=True)
        #流式读取
        def send_file():
            store_path = fullfilename
            with open(store_path, 'rb') as targetfile:
                while 1:
                    data = targetfile.read(20 * 1024 * 1024)   # 每次读取20M
                    if not data:
                        break
                    yield data
    
        response = Response(send_file(), content_type='application/octet-stream')
        response.headers["Content-disposition"] = 'attachment; filename=%s' % filename   # 如果不加上这行代码,导致下图的问题
        return response

    

路由系统

@app.route('/user/<username>')
@app.route('/post/<int:post_id>')
@app.route('/post/<float:post_id>')
@app.route('/post/<path:path>')
@app.route('/login', methods=['GET', 'POST'])

route源码

 def route(self, rule, **options):
        def decorator(f):
            endpoint = options.pop('endpoint', None)
            self.add_url_rule(rule, endpoint, f, **options)
            return f
        return decorator
  1. @app.route("/", methods=['GET'], endpoint='n1')
    def index()
    
app.add_url_rule(rule="/", endpoint="n1", view_func=index, methods=['GET'])
  1. class IndexView(views.MethodView):
       	methods = ['GET']
       	decorators = [auth, ]
        def get(self):
            return "index.GET"
        def post(self):
            return "Index.POST"
        
        def dispatch_request(self):  # 自己写分发
            print("Index")
            return "Index"
    
app.add_url_rule("/index", view_func=IndexView.as_view(name='index')) # name=endpoint

子域名
subdomian
windows: c:\windows\system32\drivers\etc
linux: /etc/hosts

@app.route("/dynamic", subdomain="<username>")
def username_index(username):
	return username + ".your-domian.tld"

@app.route("/", subdomain="admin")

自定义第三方库

实现init_app()

app.config.setdefault('XXX', ...)
app.extensions['xxx'] = ...

url 支持正则

1.写类

class RegexConverter(BaseConverter):
	def __init__(self, map, regex):
        ...
    def to_python(self, value):
        """路由匹配成功后,先执行to_python处理一下"""
        return value

    def to_url(self, value):
        val = super().to_url(value)
        return val

2.将类添加到flask 中

app.url_map.converters['regex'] = RegexConverter
@app.route("/index/<regex("\d+"):nid>")
def index(nid):
	url_for("index", nid=90)
	return "index"

装饰器:
@functools.wraps(func) # 帮助保存函数的元信息,像帮助信息


请求扩展(类似django中间件)

@app.before_request
def process_request(*args, **kwargs):
	user = session.get("user_info")
	if user or request.path == "/login":
		return None
	return redirect("/login")


@app.after_request
def process_response(response):
	return response

# 可以写多个,before#按函数的顺序执行, after倒序执行
# 请求拦截后,response还是都执行了

@app.before_first_request

@app.teardown_request

flash

基于session.pop()

@app.errorHandler(404):
def error_404(arg):
	return "my 404"

html模板定制

@app.template_global()
def sb(a1,a2):

@app.template_filter()
def db(a1,a2,a3)

# 调用方式: {{sb(1,2)}} {{ 1 | db(2,3) }}

中间件

app.run()  //
	run_simple(host, port, self)
	app.__call__()
		return self.wsgi_app(environ, start_response)

class Md:
	def __init__(self, old_wsgi_app):
		self.old_wsgi_app = old_wsgi_app

	def __call__(self, environ, start_reponse):
		print("start ...")
		ret = self.old_wsgi_app(environ, start_reponse)
		print("end ...")
		return ret

app.wsgi_app = Md(app.wsgi_app)
app.run()

蓝图

flask_pro/
	|-- templates
	|---views/
	|     |---__init__.py
		  |					from flask import Flask
		  | 				app = Flask(__name__)
		  |					from . import account
	|	  |---account.py      from . import app
	|---app.py     from views import app  app.run()

pro_flask
   |---pro_flask
   |      |----static
   |      |----views
   |            |---account.py
   |            |---__init__.py
   |---run.py
# account.py
from flask import Blueprint
#account = Blueprint("account", __name__)
account = Blueprint("account", __name__,url_prefix="/acc",
					template_folder="tpls")

@account.route("/login")
def login():

# __init__.py
from .views.account import account
app.register_blueprint(account)

-------- threading.local() -------------------

用于为每个线程开辟一块空间来保存自己的值

request源码
-- 单进程多协程

flask.globals
request = LocalProxy()

werkzeug/local.py
try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident

1.偏函数

import functools

def func(a,b)

new_func = functools.partial(func, 666)
new_func(999)

上下文

app.run()
	__call__
	1.ctx = self.request_context()
			return RequestContext(environ)
		RequestContext()
				__init__()
			Request()

2.ctx.push()
		_request_ctx_stack.push(self) //
			stack.append()

3.视图函数

4.ctx.auto_pop(error)
	 pop()
		stack.pop()

数据库连接池

http://www.cnblogs.com/wupeiqi/articles/8184686.html

1.不能为每个用户创建一个连接
2.创建一定数量的连接池。

DBUtils 模块,实现连接池

模式一: 为每个线程创建连接
模式二: 创建n个连接,多线程来时,去获取。

1.
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://username:password@localhost/db_name'
app.config['SQLALCHEMY_POOL_SIZE'] = 20
db = SQLAlchemy(app)

作业:
1. 上下文源码流程

2. 找源码 
	- before_first_request
	- before_request 
	- after_request 
	
	实施:
		- 代码刚启动?
		- 请求到来时? chain
		
	找路由匹配
	
3. 分组实现: 会议室预定
	- 设计表
	- 基于蓝图, 基于before_request是如何实现用的登录

上下文管理

​ - threading.local/ Local类,其中创建了一个字典{greelet做唯一标识:存数据}保证数据隔离
​ - 请求进来
​ - 请求相关所有数据封装到RequestContext中
​ - 再将RequestContext对象添加到Local中(通过LocalStack将对象添加到Local对象中)
​ - 使用,调用request
​ - request.method, print(request), request+xxx...会执行执行LocalProxy中对应 的方法
​ - 函数
​ - 通过LocalStack去Local中获取值.
​ - 请求终止
​ - 通过LocalStack的pop方法,Local中将值移除。

​ 3. DBUtils, 数据库连接池。


​ ctx = self.request_context(environ)
​ ctx.push() Local()--> self.storage[self.ident_func()]["stack"]


​ request = LocalProxy(partial(_lookup_req_object, 'request'))
​ 执行_lookup_req_object,自动传递request参数
​ object.setattr(self, '_LocalProxy__local', local)
​ ==> self.__local = _lookup_req_object('request')

​ session = LocalProxy(partial(_lookup_req_object, 'session'))

​ 目标: 去local中获取ctx,然后在ctx中获取request

请求上下文 : RequestContext
- request
- session
应用上下文 : AppContext
- app(current_app)
- g

 from flask import g

app_ctx = AppContext()
app_ctx.app
app_ctx.g 

多线程:

local保存数据时,使用列表创建出来的栈

多app应用

web访问多app应用时,上下文管理是如何实现?  同单app一样。

为什么要使用离线脚本?
- 如果是web运行环境,栈中永远保存1条数据(可以不用栈)
- 写脚本获取app信息时,可能存在app上下文嵌套时。

g: 每个请求周期都会创建一个用于在请求周期中传递值的一个容器。

信号

http://www.cnblogs.com/wupeiqi/articles/8249576.html

pip3 install blinker

a. before_first_request
b. 触发request_started 信号
c. before_request
d. 模板渲染
渲染前的信号: before_render_template
渲染后的信号: template_rendered.send()
e. after_request
f. session.save_session()
q. 触发request_finished信号

错误触发信号: got_request_exception.send(self, execption=e)
h. 触发信号 request_tearing_down.send(self, exc=exc)

flask-session

https://www.cnblogs.com/wupeiqi/articles/7552008.html


​ - session处理机制
​ - 请求刚到来: 获取随机字符串,存在刚去数据库中获取原来的个人数据,否则创建
​ 一个空容器 --> 内存: 对象(随机字符串)


self.app.open_session --> SecureCookieSession()

session['xxx'] = 123

在local的ctx中找到session,在空字典中写值

请求刚来self.session = None
ctx.push()
self.session = session_interface.open_session(self.app, self.request)

视图函数:
self.dispatch_requeset()
self.finalize_request()
self.process_response()
self.session_interface.save_session(self, ctx.session, response)
response.set_cookie("session", "将数据加密之后写到cookie中")

cookie:
内容: key, value, 过期时间, 路径和域
如果设置了过期时间就保存在硬盘里。

goole设置cookie的响应头的例子:
HTTP/1.1 302 Found 
Location: http://www.google.com/intl/zh-CN/
Set-Cookie:PREF=ID=0565f77e132de138:NW=1:TM=1098082649:\
			LM=1098082649:S=KaeaCFPo49RiA_d8; 
expires=Sun, 17-Jan-2038 19:14:07 GMT; 
path=/; domain=.google.com
Content-Type: text/html

session:
cookie可以被人为的禁止,
1. 使用的一种技术叫做URL重写,就是把session id直接附加在URL路径的后面,附加方式也有两种,
(1)一种是作为URL路径的附加信息,表现形式为http://...../xxx;jsessionid=ByOK ... 99zWpBng!-145788764
(2)是作为查询字符串附加在URL后面,表现形式为http://...../xxx?jsessionid=ByOK ... 99zWpBng!-145788764

2.表单隐藏字段。就是服务器会自动修改表单,添加一个隐藏字段,以便在表单提交时能够把session id传递回服务器

何时被删除:
	于关闭浏览器不会导致session被删除,迫使服务器为seesion设置了一个失效时间

1.方式一
from redis import Redis
redis = Redis()
app.session_interface = RedisSessionInterface(redis, key_prefix="__")
# permanent=False, 关闭浏览器cookie失效。 持久化

2.方式二
from redis import Redis
from flask.ext.session import Session

app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = Redis(host='192.168.0.94',port='6379')
Session(app)

1.内置原理
2.自定义
3.flask_session

wtforms

https://www.cnblogs.com/wupeiqi/articles/8202357.html

fabric
ansible
salstack

new 真正创建对象:
什么时候用到了__new__:
- 单例
- WTForms,当前对象/UnboundField对象
- rest_framework序列化
many=True
many=False

__init__对对象做初始化

SQLAlchmey

http://www.cnblogs.com/wupeiqi/articles/8259356.html

ORM框架 Object Relational Mapper 关系对象映射

类/对象操作 --> SQL --> pymysql,MySQLdb --> 再在数据库中执行

http://www.cnblogs.com/wupeiqi/articles/8259356.html

pip3 install sqlalchemy

schema: 提要,纲要,图式

ORM操作

session=Session()
#session=scoped_session()

obj1 = models.User(name="alex1", email="alex1@xx.com")
session.add(obj1)
session.commit()

session.close()

查询

Role.query.filter(Role.id>1).all()
role = Role.query.filter_by(name=r).first()	

from sqlalchemy import text
Role.query.filter(text('id>1')).all()

# filter与filter_by的区别:
#filter用类名.属性名,比较用==,filter_by直接用属性名,比较用=
#filter: 比filter_by的功能更强大,支持比较运算符,支持or_、in_等语法。
query.filter(and_(IS.node == node, IS.password == password))
session.query(IS).filter(IS.node == node).filter(IS.password == password).all()
	
#filter_by:   不支持比较运算符。
session.query(IS).filter_by(node=node, password=password).all()

session.query(Users).from_statement(text("SELECT * FROM users where name=:name")).params(name='ed').all()

Flask 使用数据库

Flask-SQLAlchemy 扩展
pip install flask-sqlalchemy

#配置数据库的地址
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:密码@127.0.0.1:3306/test' 
#跟踪数据库的修改 --> 不建议开启,未来会移除
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAchemy(app)

class Role(db.Model):
    #定义表名
    __tablename__ = 'roles'
    #定义字段
    id = db.Column(db.Integer,primary_key=True)
    name = db.Column(db.String(16),unique=True)
    #在一的一方,写关联 
	#users = db.relationship('User')
    users = db.relationship('User',backref='Role')
from flask_SQL import Role, db
role = Role(name="admin")
db.session.add(role)
db.session.commint()    # 提交后数据库才会改变

# 改:
user.name = 'chenxuyuan' 
db.session.commit()
# 删:
db.session.delete(user)
db.session.commit()

# 回退
db.session.rollback()

模型之间的关联

一对多:
db.relationship()

过滤器:
filter() #User.query.filter(User.id==4).first()
filter_by() #User.query.filter_by(id=3).first()
limit
offset()
order_by()

all() #User.query.all()
first()
first_or_404()
get()
get_or_404()
count()
paginate()

count()查询:

flask-migrate

from flask_migrate import Migrate, MigrateCommand
migrate = Migrate(app, db)	   # db = SQLAlchemy()

数据库迁移命令

python manager.py db init 
python manager.py db migrate 
python manager.py db upgrade

manager.add_command('db', MigrateCommand)

flask-script

from flask_scritp import Manager 
manager = Manager(app)
manager.run() 
python3 manager.py runserver 
@manager.command
def custom(arg):
	"""
	自定义命令
	python manager.py custom 123
	"""
	print(arg)
	
@manager.option('-n', '--name', dest='name')
@manager.option('-u', '--url', dest='url')
def cmd(name, url):
    """ python manager.py cmd -n tom -u http://www.tom.com 
	"""

《Flask Web开发:基于Python的Web应用开发实战》

pip3 install flask_script

export FLASK_APP=hello.py
export FLASK_DEBUG=1

flask_run

上下文:
应用上下文:
current_app
g
请求上下文:
request
session

使用这些变量要先激活:

from flask import current_app
app_ctx = app.app_context()  *
app_ctx.push()               * 激活
current_app.name
app_ctx.pop()

请求调度:
app.url_map
<Rule '/index' (HEAD, OPTIONS, GET) -> index>

请求钩子

​ before_first_request
​ before_request
​ after_request
​ teardown_request

响应:
1.
​ return '

Bad Request

', 400

视图函数返回的响应还可接受第三个参数 header组成的字典

from flask import make_reponse 

response = make_response('<h1> This document carries a cookie</h1>')
response.set_cookie('answer', '42')
return response
  1. reedirect

  2. abort(404)

模板: jinja2

渲染模板:
render_template
常用过滤器:
safe 渲染值时不转义
capitalize 首字母大写 ,其他小写
lower
upper
title 每个单词首字母大写
trim 首尾空格去掉
striptags 渲染之前把值中的所有HTML标签都删掉

控制结构

{% if user %}
	...
{% else %}
	   ...
{% endif %}

for:
{% for i in com %}
	...
{% endfor %}

{% macro render_comment(comment) %}
	<li> {{ comment }} </li>
{% endmacro %}

在模板中导入 
{% import 'macros.html' as macros %}
{{ macros.render_comment(comment) }}
{% endfor %} 

模板

base.html

<html>

<head>
{ % block head % }
<title>{ % block title % }{ % endblock % } - My Application</title>
{ % endblock % }
</head>

<body>
{ % block body % }
{ % endblock % }
</body>
</html>

使用模板

{ % extends "base.html" % }
{ % block title % }Index{ % endblock % }
{ % block head % }
{{ super() }}

<style>
</style>
{ % endblock % }

{ % block body % }

<h1>Hello, World!</h1>
{ % endblock % }

链接

url_for()
url_for('index')
url_for('index', _external=True)   #  返回的是绝对地址 http://localhost:5000/
url_for('user', name='john', _external=True)
url_for('index', page=2)  # 返回: /?page=2

flash('Looks')
get_flashed_messages()

flask-migrate

from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()
migrate.init_app(app, db)
flask db init      # 生成migrate目录 
flask db migrate   # 自动创建一个迁移脚本  -m "message"
flask db upgrade   # 把迁移应用到数据库中

email

配置flask_mail

app.config['MAIL_SERVER'] = 'smtp.163.com'
app.config['MAIL_PORT'] = 25
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')

flask shell

from flask_mail import Message
from hello import mail
msg = Message('test email', sender='you@example.com', recipients=['you@example.com'])
msg.body = 'This is the plain text body'
msg.html = 'This is the <b>HTML</b> body'
with app.app_context():
	mail.send(msg)

flask-login

保护路由:

from flask_login import login_required 
@app.route('/secret')
@login_reuqired
def secret():
    return "Only authenticated users are allowed!"

flask 插件

  • flask_themes: 皮肤,博客必不可少的
  • flask_sqlalchemy: flask对sqlalchemy的插件,定义了一些方法,使创建models和输出query更方便
  • flask_wtf: 对wtforms的插件,默认加入了csrf功能(防止表单重复提交)和Recaptcha(验证码)
  • flask_uploads: 上传文件的插件
  • flask_cache: 缓存插件(支持memcached,gaememcached,filesystem,simple等)
  • flask_principal: 权限插件 (众多插件中比较复杂的一个, 但也是作用很大的一个),支持各种权限方式,较django admin的权限,我只能说,这个插件让你知道,权限其实很简单。
  • flask_mail: 发送邮件插件
  • flask_script: 项目管理插件,类似django的manager
  • flask_babel: 多语言支持,使用非常方便,(request.accept_languages.best_match判断语言有点怪,好象会根据系统语言判断,待深究)
  • PyCasbin
  • flask_admin
  • flask_login

Django

创建工程

django-admin startproject demo

demo
├── demo
│   ├── init.py
│   ├── settings.py
│   ├── urls.py 项目的URL配置文件。
│   └── wsgi.py 项目与WSGI兼容的Web服务器入口
└── manage.py 是项目管理文件,通过它管理项目。

# 运行
python manage.py runserver ip:port

创建子应用模块

python manage.py startapp users

users
├── admin.py 文件跟网站的后台管理站点配置相关
├── apps.py 文件用于配置当前子应用的相关信息。
├── __init__.py
├── migrations 用于存放数据库迁移历史文件。
│   └── __init__.py
├── models.py 用户保存数据库模型类。
├── tests.py
└── views.py 编写Web应用视图。

注册安装子应用

# settings.py 
INSTALLED_APPS = [
    ...
    'users.apps.UserConfig',
]

配置数据库

settings.py

# 默认配置
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

配置语言与时区

​ LANGUAGE_CODE = 'zh-hans'
​ TIME_ZONE = 'Asia/Shanghai' // 大小写

静态文件配置

STATIC_URL = '/static/'
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static_files'),
]

from django.conf import settings
from django.conf.urls.static import static

urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

链接 URL 映射器

urlpatterns = [
    path("admin/", admin.site.urls),
    path('catalog/', include('catalog.urls')),
    path('', RedirectView.as_view(url='/catalog/')), # 将首页重定向到catalog
]

运行数据库迁移

python3 manage.py makemigrations
python3 manage.py migrate

运行

python3 manage.py runserver

Django管理员站点

注册模型

from .models import Author, Genre, Book, BookInstance

admin.site.register(Book)
admin.site.register(Author)
admin.site.register(Genre)
admin.site.register(BookInstance)

创建超级用户

python3 manage.py createsuperuser

注册一个ModelAdmin类

/locallibrary/catalog/admin.py

# Define the admin class
class AuthorAdmin(admin.ModelAdmin):
    # 配置列表视图
    list_display = ('last_name', 'first_name', 'date_of_birth', 'date_of_death')
admin.site.register(Author, AuthorAdmin)
	# 控制哪些字段被显示和布局, 元组是分组
    fields = ['first_name', 'last_name', ('date_of_birth', 'date_of_death')]

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'display_genre')
    # 需要在Book模型中添加display_genre函数
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
    summary = models.TextField(max_length=1000, help_text="Enter a brief description of the book")
    isbn = models.CharField('ISBN',max_length=13, help_text='13 Character <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>')
    genre = models.ManyToManyField(Genre, help_text="Select a genre for this book")
    
    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('book-detail', args=[str(self.id)])
    
	def display_genre(self):
        return ', '.join([ genre.name for genre in self.genre.all()[:3] ])
    display_genre.short_description = 'Genre'

添加列表过滤器

class BookInstanceAdmin(admin.ModelAdmin):
    list_filter = ('status', 'due_back')
    
    # 使用 fieldsets 属性添加“部分”以在详细信息表单中对相关的模型信息进行分组
    fieldsets = (
        (None, {
            'fields': ('book','imprint', 'id')
        }),
        ('Availability', {
            'fields': ('status', 'due_back')
        }),
    )

关联记录的内联编辑

# 通过声明 inlines, 类型 TabularInline (水平布局 ) or StackedInline (垂直布局,就像默认布局) 
class BooksInstanceInline(admin.TabularInline):
    model = BookInstance

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    list_display = ('title', 'author', 'display_genre')
    inlines = [BooksInstanceInline]

路由

#2.X: 
from django.urls import path, include, re_path
path('index-<int:article_type_id>-<int:category_id>.html',views.index)
re_path就是1.X中的url('^/usrs')
url('index-(?P<article_type_id>\d+)-(?P<category_id>\d+).html',views.index)


path 不要^$等符号,类似flask
    int
    str
    slug  ASCII或数字组成的,包括-_, 
    uuid  必须包含短划线并且字母必须为小写
    path
    
    
# 名字空间:
url(r'^users/', include('users.urls', namespace='users')),
# 名字:
url(r'^index/$', views.index, name='index'),

# reverse反解析
url = reverse('users:index')

# eg:
path(r"detail/<yyyy:id>", detail, name='detail')
url = reverse('index', kwargs={"id": 1992})  # 没有kwargs会报错

自定义转化器

class FourDigitYearConverter:
    regex = '[0-9]{4}'
    def to_python(self, value):
        return int(value)

    def to_url(self, value):
        return "%04d"%value
# 注册
from django.urls import register_converter
register_converter(FourDigitYearConverter, "yyyy")

图标

from Django.views.generic.base import RedirectView

urlpatterns=[
	url(r'^favicon.ico$',RedirectView.as_view(url=r'static/favicon.ico')), 
]

请求

# 1.URL路径参数:
url(r'^weather/(?P<city>[a-z]+)/(?P<year>\d{4})/$', views.weather),
def wearther(request, year, city):
    ...
# 2.查询字符串Query String  QueryDict对象 
request.GET.get('a')   # flask:request.arg.get()

# 3.请求体:
	表单类型: Form Data  
		request.POST.get('a')  # flask: request.form.get()
		
	非表单类型 Non-Form Data
		json_str = request.body
		req_data = json.loads(json_str)
		
4.请求头
	request.META
	
其他常用HttpRequest对象
	method
	user
	path
	encoding
	FILES

响应

# 1.django.http.HttpResponse
	HttpResponse(content=响应体, content_type=响应体数据类型, status=状态码)
# 2.HttpResponse子类: (快速设置状态码)
	HttpResponseRedirect 301

# 3.JsonResponse
	from django.http import JsonResponse

# 4.redirect
	from django.shortcuts import redirect
# 设置Cookie: 
HttpResponse.set_cookie(cookie名, value=cookie值, max_age=cookie有效期)
eg: response.set_cookie('itcast2', 'python2', max_age=3600) 
# 读:
request.COOKIES.get('itcast2')	

Session

# 启用Session:
默认启用:
setttings.py  MIDDLEWARE = [
    'django.contrib.sessions.middleware.SessionMiddleware' 
]
#存储方式:
# 1.数据库:
SESSION_ENGINE='django.contrib.sessions.backends.db' (默认)
# 本地缓存     .cache/.cache_db
INSTALLED_APPS = ['django.contrib.sessions']
  1. Redis /etc/redis/redis.conf
    在redis中保存session,需要引入第三方扩展,我们可以使用django-redis来解决。

(1) 安装扩展

pip install django-redis

(2) 配置

# 在settings.py文件中做如下设置
CACHES = {
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"

# 操作:
request.session['key'] = value

# 清理:
reqeust.session.clear()  删除值部分
# 清除session数据: 
request.session.flush()

# 删除指定键及值
del request.session['key']

# 设置session的有效期: 
request.session.set_expiry(value)
# 记录访问次数
num_visits = request.session.get("num_visits", 0)
request.session["num_visits"] = num_visits + 1

创建视图

类视图

from django.views.generic import View
class RegisterView(View):
	def get(self,request):
		...
	def post(self, request):
		...

# 路由: 
url(r'^register/$', views.RegisterView.as_view(), name='register')

#使用装饰器:
	#1.在URL配置中装饰:
		 url(r'^demo/$', my_decorate(DemoView.as_view()))
	#2.在类视图中装饰:
		from django.utils.decorators import method_decorator
		@method_decorator(my_decorator, name='dispatch') // name为指定装饰的函数
	
		@method_decorator(my_decorator)  # 为get方法添加了装饰器
		def get(self, request):
			print('get方法')
			return HttpResponse('ok')
			
#类视图Minxin扩展类
	class ListModeMixin(object):
		def list(self, request,*args, **kwargs):
			...
	class BooksView(ListModeMixin, View):
		...

中间件

​ 定义方法: 在users应用中新建一个middleware.py文件

def my_middleware(get_response):
		# 此处编写的代码仅在Django第一次配置和初始化的时候执行一次。
def middleware(request):
		# 此处编写的代码会在每个请求处理视图前被调用。
        ...
		response = get_response(request)
		# 此处编写的代码会在每个请求处理视图之后被调用。
        ...
		return respons
	return middleware
	
# 定义好中间件后,需要在settings.py 文件中添加注册中间件
	MIDDLEWARE = [
		'django.middleware.my_middleware',
	]

多个中间件的执行顺序:
	在请求视图被处理前,中间件由上至下依次执行
	在请求视图被处理后,中间件由下至上依次执行
	
class MiddlewareMixin:
	def __init__(self, get_response=None):
		self.get_response = get_response
		super().__init__()

	def __call__(self, request):
		response = None
		if hasattr(self, 'process_request'):
			response = self.process_request(request)
		response = response or self.get_response(request)
		if hasattr(self, 'process_response'):
			response = self.process_response(request, response)
		return response
		
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse,redirect

class middle_ware1(MiddlewareMixin):

    def process_request(self,request):
        if request.path_info == "/login/":
            return None

        if not request.session.get("user_info"):
            return redirect("/login/")

    def process_response(self,request,response):
        print("middle_ware1.process_response")
        return response
        
 列举django中间件的5个方法:

	#1.process_request : 请求进来时,权限认证
	#2.process_view : 路由匹配之后,能够得到视图函数
	#3.process_exception : 异常时执行
	#4.process_template_responseprocess : 模板渲染时执行
	#5.process_response : 请求有响应时执行
	
# 应用场景 
1.IP访问频率限制 
2.URL访问过滤

模板

​ 配置: settings.py

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],  # 此处修改
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
	
<html lang="en">

<head>
    {% block title %}<title>Local Library</title> {% endblock %}
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.3.3/js/bootstrap.min.js"></script>

    {% load static %}
    <link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}">
</head>

<body>
    <div class="container-fluid">
        <div class="row">
            <div class="col-md-2">
                {% block sidebar %}
                <ul class="sidebar-nav">
                    <li><a href="{% url 'index' %}">Home</a></li>
                    <li><a href="{% url 'books' %}">All Books</a></li>
                    <li><a href="{% url 'authors' %}">All Authors</a></li>
                </ul>
                {% endblock %}
            </div>
            <div class="col-md-10">
                {% block content %}{% endblock %}

                {% block pagination %}
                {% if is_paginated %}
                <div class="pagination">
                    <span class="page-links">
                        {% if page_obj.has_previous %}
                        <a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">previous</a>
                        {% endif %}
                        <span class="page-current">
                            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
                        </span>
                        {% if page_obj.has_next %}
                        <a href="{{ request.path }}?page={{ page_obj.next_page_number }}">next</a>
                        {% endif %}
                    </span>
                </div>
                {% endif %}
                {% endblock %}

            </div>
        </div>
    </div>
</body>

</html>

数据库

配置

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'HOST': '127.0.0.1',  # 数据库主机
        'PORT': 3306,  # 数据库端口
        'USER': 'root',  # 数据库用户名
        'PASSWORD': 'mysql',  # 数据库用户密码
        'NAME': 'django_demo'  # 数据库名字
    }
}

Model

from django.db import Models 

models.Model 

IntegerField 
BooleanField
CharField      (max_length, default)
DecimaField    (max_digits=5, decimal_places=2)
ImageField 
EmailField
gtype = ForeignKey(TypeInfo, on_delete=models.CASCADE, verbose_name="分类")
HTMLField   // from tinymce.models import HTMLField

class Meta:
	verbose_name = "商品"
	table_name = "goods_info"

数据库操作

增: 1.book = BookInfo(btitle='sql',...)
		  book.save()
	2. BookInfo.objects.create(hname='john',...)
删:	book = BookInfo.objects.get(id=1)
     book.delete()
  
改:  1. book = BookInfo.objects.get(id=1)
		 book.btitle='mysql'
		 book.save()
	  2. BookInfo.objects.filter(id=1).update(btitle='射雕')
	  
查:  
	基本查询: get ,all, count 
	过滤查询: 
		filter
		exclude  排除符合条件剩下的结果 
		get      过滤单一结果 

		属性名称__比较运算符=值
		
		1)相等
			exact:   相等 
			BookInfo.objects.filter(id__exact=1) , 简写id=1
		2)模糊查询
			contains  包含
			startswith, endswith
			
			算符前加上i表示不区分大小写,如iexact、icontains、istartswith、iendswith
		
		3)空查询 
			isnull
			BookInfo.objects.filter(btitle__isnull=False)
		4)获取查询
			in
			BookInfo.objects.filter(id__in=[1, 3, 5])
		5)比较查询:
			gt
			gte
			lt
			lte
			不等于的运算符,使用exclude()过滤器。
		
		6)日期查询:
			year、month、day、week_day、hour、minute、second
			
			BookInfo.objects.filter(bpub_date__year=1980)
			BookInfo.objects.filter(bpub_date__gt=date(1990, 1, 1))
			
	F对象:
		两个属性怎么比较呢? 
		from django.db.models import F
		BookInfo.objects.filter(bread__gte=F('bcomment'))
		
	Q对象:
		多个过滤器逐个调用表示逻辑与关系,同sql语句中where部分的and关键字。
	
		BookInfo.objects.filter(bread__gt=20,id__lt=3)
		或
		BookInfo.objects.filter(bread__gt=20).filter(id__lt=3)
		
		Q对象可以使用&、|连接,&表示逻辑与,|表示逻辑或
		BookInfo.objects.filter(Q(bread__gt=20) | Q(pk__lt=3))
		~ 不等于
		BookInfo.objects.filter(~Q(pk=3))

	聚合函数
		Avg 平均,Count 数量,Max 最大,Min 最小,Sum 求和,被定义在django.db.models中
		BookInfo.objects.aggregate(Sum('bread'))
		
	排序:
		order_by
			BookInfo.objects.all().order_by('-bread')  # 降序
            
    关联查询:
​			一对多:
​				查询某本书所有英雄:
​					b = BookInfo.objects.get(id=1)
​					b.heroinfo_set.all()
​			多对一:
​				某个英雄属于哪本书
​				h = HeroInfo.objects.get(id=1)
​				h.hbook
​			
​	关联过滤查询:	
​			关联模型类名小写__属性名__条件运算符=值
​			查询图书,要求图书英雄为"孙悟空"
​			BookInfo.objects.filter(heroinfo_hname='孙悟空')
​			BookInfo.objects.filter(heroinfo_hname='孙悟空')
​				
​			查询书名为“天龙八部”的所有英雄。
​			HeroInfo.objects.filter(hbook__btitle='天龙八部')
​	
​			查询图书阅读量大于30的所有英雄
​			HeroInfo.objects.filter(hbook__bread__gt=30)

分页

from django.core.paginator import Paginator
​		1.Paginator
​			p = Paginator(a1, 10) # a1可以是列表,查询集
​				常用属性:
​					count
​					num_pages
​					page_range 下标从 1 开始的页数范围迭代器。
​		2.Page对象 
​			books = p.page(1)   // books[0]
​				常用属性:
​					object_list  
​					number       当前页序号
​					paginator
​				常用函数:
​					has_next()
​					has_previous()
​					has_other_pages()  #  是否有上或下一页
​					next_page_number()
​					previous_page_number()  


		

查看 SQL语句

​ 1) QuerySet有query属性,只能查看select
​ 2)

from django.db import connection 
connection.queries

​ 执行SQL语句
​ 1)

c = connection.cursor()
c.execute("""...""")
c.fetchone()

​ 2)
BookInfo.objects.raw("""select * from tb_books")
必须要包含BookInfo的主键

查询集QuerySet
	all()
	filter()
	exclude()
	order_by()  会返回查询集
	
	两大特性:
		惰性执行
			qs = BookInfo.objects.all()
			for book in qs:   -->.才真正去数据库查询
		缓存
			[ book.id for book in qs]
		
管理器Manager
	自定义管理器
	1. 修改原始查询集,重写all()方法。
	a)打开booktest/models.py文件,定义类BookInfoManager
	#图书管理器
	class BookInfoManager(models.Manager):
		def all(self):
			#默认查询未删除的图书信息
			#调用父类的成员语法为:super().方法名
			return super().filter(is_delete=False)
	b)在模型类BookInfo中定义管理器
	class BookInfo(models.Model):
		...
		books = BookInfoManager()
	c)使用方法
	BookInfo.books.all()

django请求的生命周期

  1. wsgi,请求封装后交给web框架 (Flask、Django)
  2. 中间件,对请求进行校验或在请求对象中添加其他相关数据,例如:csrf、request.session
  3. 路由匹配 根据浏览器发送的不同url去匹配不同的视图函数
  4. 视图函数,在视图函数中进行业务逻辑的处理,可能涉及到:orm、templates => 渲染
  5. 中间件,对响应的数据进行处理。
  6. wsgi,将响应的内容发送给浏览器。

FBV: function base view
CBV: class

用户授权与许可

# 配置  默认已配置
INSTALLED_APPS = [
    ...
    'django.contrib.auth', 
    'django.contrib.contenttypes',]

MIDDLEWARE = [
    ...
    'django.contrib.sessions.middleware.SessionMiddleware', 
    ...
    'django.contrib.auth.middleware.AuthenticationMiddleware', 
    ....

tornado

========================================================================

使用场景:
用户量大,高并发
大量的HTTP持久连接

C10K :
Concurently handling ten thousand connections.

少而精
异步非阻塞的设计方式

特点:
HTTP服务器
异步编程
WebSockets

app=tornado.web.Application([(),])
app.listen(8000)  #只是绑定端口。  只能在单进程模式中使用

tornado.ioloop.IOLoop.current().start()   #IOLoop.current(): 返回当前线程的IOLoop实例。
										  #.start() 启动,同时开启监听。
=== handler ======================= 请求处理
===================================
=== tornado.web.Application ======= 路由映射表
===================================
=== tornado-IOLoop ================
===================================
=== linux-epoll =================== 监听socket
===================================

httpserver

import tornado.httpserver
httpServer = tornado.httpserver.HTTPServer(app)
httpServer.bind(8000)
httpServer.start(4)         启动4个进程。 
tornado.ioloop.IOLoop....

不建议使用上面方式启动多进程。
问题:
1.每个子进程都会从父进程中复制一份IOLoop的实例,如果在创建子进程前修
改了IOLoop,会影响所有的子进程。
2.所有进程都是由一个命令启动的,无法做到在不停止服务的情况下修改代码。
3.所有进程共享一个端口,想要分别监控很困难。

而是手动启动。多几个server.py

options

提供全局参数的定义、存储、转换。

from tornado.options import define,options

define(name,default=None,type=None,help=None,
metavar=None,multiple=False,group=None,callback=None)

取出值
options.name

type:变量类型,根据default类型进行转换,转换不成会报错。如果没有default,则不转换
可以有str,float,int,datetime,timedelta

multiple:
设置选项变量是否可以为多个值,默认为False

获取参数的方法

1.tornado.options.parse_command_line()   #将命令行里的参数转换成
---- 不建议用

2.tornado.options.parse_config_file(path)  
	从配置文件导入参数。
	书写格式仍需要按照python格式写
	不支持字典。
	
3.config.py
	options={
		"port":8000,
		"list":["good","nice","handsome"]
	}
	settings={
		"autoreload":True,    	#自动重启。
		"debug":False,		 	
	}

日志:
当我们在代码中使用parse_command_line()或者parse_config_file(path)方法
时,tornado会默认开启logging模块功能,向屏幕终端输入一些打印信息
关闭日志:
1.--logging=none
2.tornado.options.options.logging=None #在导入配置文件前执行。

settings:
debug:
设置tornado是否工作在调试模式下,默认为False,即在工作生产模式下
True:会监控源代码文件,当有保存改动时会重新启动服务器。 --->autoreload=True
取消缓存编译的模板 (可单独设置:compiled_template_cache=False)
取消缓存静态文件的hash值 (static_hash_cache=False)
提供追踪信息。 (serve_traceback=True)

static_path:
template_path:

传参:
def initialize(self,word1,word2)

反向解析:
路由:

tornado.web.url()   name=""

self.reverse_url("...")

tornado.web.RequestHandler:
利用HTTP协议向服务器传递参数:
1.提取uri的特定部分
(r'/liu/(\w+)/(\w+),index.LiuHandler) # (?P\w+)
get(self,h1,h2)
2.get方式传递参数
127.0.0.1:8000/zhang?a=1&b=2&c=3:
self.get_query_argument(name,default=ARG_DEFAULT,strip=True)
strip:是否过左右两边的空白字符,默认为True.
list = self.get_query_arguments()
3.post方式传递参数
self.get_body_argument(name,default=ARG_DEFAULT,strip=True)
4.可以获取get,post请求,
self.get_argument() s
5.在http报文的头中增加自定义的字段。

tornado 		
========================================================================

使用场景:
	用户量大,高并发
	大量的HTTP持久连接
	
C10K : 
	Concurently handling ten thousand connections.

少而精
异步非阻塞的设计方式

特点:
	HTTP服务器
	异步编程 
	WebSockets


app=tornado.web.Application([(),])
app.listen(8000)  #只是绑定端口。  只能在单进程模式中使用
​
tornado.ioloop.IOLoop.current().start()   #IOLoop.current(): 返回当前线程的IOLoop实例。
                                          #.start() 启动,同时开启监听。

=== handler ======================= 请求处理
===================================
=== tornado.web.Application ======= 路由映射表
===================================
=== tornado-IOLoop ================
===================================
=== linux-epoll =================== 监听socket
===================================
httpserver 

import tornado.httpserver
httpServer = tornado.httpserver.HTTPServer(app)
httpServer.bind(8000)
httpServer.start(4)         启动4个进程。 
tornado.ioloop.IOLoop....
不建议使用上面方式启动多进程。
	问题:
		1.每个子进程都会从父进程中复制一份IOLoop的实例,如果在创建子进程前修
			改了IOLoop,会影响所有的子进程。
		2.所有进程都是由一个命令启动的,无法做到在不停止服务的情况下修改代码。
		3.所有进程共享一个端口,想要分别监控很困难。
	
而是手动启动。多几个server.py

options 
提供全局参数的定义、存储、转换。

from tornado.options import define,options

define(name,default=None,type=None,help=None,
	metavar=None,multiple=False,group=None,callback=None)

取出值
options.name 
	
type:变量类型,根据default类型进行转换,转换不成会报错。如果没有default,则不转换
		可以有str,float,int,datetime,timedelta
		
multiple:
	设置选项变量是否可以为多个值,默认为False

获取参数的方法


1.tornado.options.parse_command_line()   #将命令行里的参数转换成
---- 不建议用
​
2.tornado.options.parse_config_file(path)  
    从配置文件导入参数。
    书写格式仍需要按照python格式写
    不支持字典。
    
3.config.py
    options={
        "port":8000,
        "list":["good","nice","handsome"]
    }
    settings={
        "autoreload":True,      #自动重启。
        "debug":False,          
    }
日志:
	当我们在代码中使用parse_command_line()或者parse_config_file(path)方法
	时,tornado会默认开启logging模块功能,向屏幕终端输入一些打印信息
	关闭日志:
		1.--logging=none
		2.tornado.options.options.logging=None   #在导入配置文件前执行。

settings:
	debug:
		设置tornado是否工作在调试模式下,默认为False,即在工作生产模式下
		True:会监控源代码文件,当有保存改动时会重新启动服务器。  --->autoreload=True
			 取消缓存编译的模板  (可单独设置:compiled_template_cache=False)
			 取消缓存静态文件的hash值 (static_hash_cache=False)
			 提供追踪信息。			(serve_traceback=True)


static_path:
template_path:
传参:
	def initialize(self,word1,word2)

反向解析:
	路由:


tornado.web.url()   name=""
​
self.reverse_url("...")
tornado.web.RequestHandler:
	利用HTTP协议向服务器传递参数: 
		1.提取uri的特定部分
			(r'/liu/(\w+)/(\w+),index.LiuHandler)  # (?P<p1>\w+)
			get(self,h1,h2)
		2.get方式传递参数
			127.0.0.1:8000/zhang?a=1&b=2&c=3:
				self.get_query_argument(name,default=ARG_DEFAULT,strip=True)
				strip:是否过左右两边的空白字符,默认为True.
				list = self.get_query_arguments()
		3.post方式传递参数
				self.get_body_argument(name,default=ARG_DEFAULT,strip=True)
		4.可以获取get,post请求,
				self.get_argument()  srequest对象:
	作用: 存储了关于请求的相关信息
	属性: 
		method : 
		host:
		uri:
		path
		query
		version
		headers
		body
		remote_ip   客户端的ip
		files   用户上传的文件,字典类型
tornador.httputil.HTTPFile:	
	属性:
		filename:
		body: 文件数据实体
		content_type: 文件类型。
	<input type="file" name="img"/>
	self.request.files={"name":[{"filename":..,
								"body":b"...",
								"content_type":'text/plain'}]  -->看着是字典,
													其实是HTTPFile对象。
						"name2":[{...}]		
						
						}


HTTP 1.1 特点: 长连接。

响应输出:
​ write:
​ 将数据写入缓冲区。 (1.程序结束,2.手动刷新,3.缓冲区满了。4.遇到\n)
​ finish: 刷新缓冲区,关闭当次请求通道。
​ 利用write方法写json数据。

set_header(name,value):
​ 作用:手动设置一个名为name,值为value的响应头字段。

set_default_headers()
def set_default_headers(self): 在进入get()之间调用。
self.set_header(...)

self.set_status(status_code,reason=None):

重定向:
redirect(url)
send_error(status_code=500,kwargs)
作用:抛出HTTP错误状态码,
write_error(status_code,
kwargs)
作用:用来处理send_error抛出的错误信息,并返回给浏览器错误界面。

接口调用顺序:
set_default_headers()
initialize() 处理参数。url后,字典。
prepare() : 预处理方法,在执行对应的请求方法之前调用。

HTTP方法: get
post
head ,类似get请求,只不过响应中没有具体的内容,报头。
delete 请求服务器删除指定的资源
put 从客户端向服务器传送指定内容
patch 请求修改局部内容
options 返回URL支持的所有HTTP方法。

write_error
on_finish() 作用:资源释放,日志记录。
尽量不要在该方法中进行响应输出。

正常:
set_default_headers
initialize
prepare
HTTP方法
on_finish
出错:
set_default_headers
initialize
prepare
HTTP方法
set_dufault_headers
write_error
on_finish

模板

配置模板路径: "template_path":os.path.join(BASE_DIRS,"templates")
render()

变量与表达式:
语法: {{ var }}
{{ expression }}
流程控制:
if
for
while

函数:
static_url()
获取配置的静态目录,并将参数拼接到 静态目录后面并返回新的路径。
href="{{static_url('home.css')}}"
优点:
修改目录不用修改url
创建了一个基于文件内容的hash值,并将其添加到url末尾。总能保存每次加载的是最新的。
自定义函数:
在handler里定义,传到模板里。 #django里也有方法,不过是对象的方法,不能传参。

转义:
默认开启了自动转义功能,防止网站被攻击。
关闭: raw : {% raw str %}
当前文档的:{% autoescape None %}
在配置中修改 setttings={"autoescape":None}
escape()函数:

继承:

{% block main %}
...
{% end %}

静态文件:
static_path:

staticFileHandler:	
	本质:是tornado预制的用来提供静态资源文件的handler。
	作用:映射静态文件	
	(r'/(.*)$',tornado.web.StaticFileHandler,{"path":os.path.join(config.BASE_DIRS,'static'),"default_filename":"index.html"})

数据库

tornado没有自带的ORM,对于数据库需要自己去适配。

应用安全

Cookie: 4kb,20条

普通Cookie:
	设置: self.set_cookie(name,value,domain=None,expires=None,path="/",expires_days=None,**kwargs)
		expires: 时间戳整数,时间元组,datetime类型,
		expires_days: 有效天数,优先级低于expires.
		
	获取: selt.get_cookie(name,default)
	
	清除cookie: 
	
		self.clear_cookie(name,path="/",domain=None)
		    作用: 删除名为name的cookie,并同时匹配path,domain
			并不是立即删除浏览器的cookie,而是给cookie值设置空,并改变其有效其为失效,
			真正删除的是浏览器自己。
			
		self.clear_all_cookies()

安全Cookie: tornado提供一种简易加密方式。
		设置: 需要为应用配置一个用来给Cookie进行混淆加密的密钥。
			  "cookie_secret":"......."  (base64.b64encode(uuid.uuid4().bytes))
			self.set_secure_cookie(name, value, expires_days=30, version=None)
			"2|1:0|10:1528168601|5:hello|8:d29ybGQ=|905b4630a06055c8b0867e0c2bcc69721d7a1412fcbf24190c0bc2f9a8b533e3"
			安全cookie的版本,2,
			默认为0
			时间戳
			cookie名
			base64编码的cookie值
			签名值,不带长度说明
			
	获取: self.get_secure_cookie(name,value=None,max_age_day=31,min_version=None)

XSRF: 跨站请求代码。
保护: 同源策略。
不用get,用post。
开启xsrf保护: setting: "xsrf_cookies":True
在模板中:
{% module xsrf_form_html()%} 为浏览器设置了_xsrf的安全cookie.
原因:为模板表单添加了一个隐藏的域,名为_xsrf,值为_xsrf的cookie的值
<input type="hidden" name="_xsrf",value=
"2|fd3fd9d|dafkfdksajfdsajf..." > (在代码中添加也能访问)

   			在非模板中:手动设置_xsrf的cookie.
   					self.xsrf_token

  function getCookie(name){
  		var cookie=document.cookie.match("\\b"+name+"([^;]*)\\b")	
  		return cook? cook[1]:undefined
  }
  
  发起ajax请求:
 <script type="text/javascript" charset="utf-8" 
	  	 	src="{{static_url('js/jquery.min.js)}}"></script>  引入js.

	  	 function login(){
	  	 	$.post("/postfile","_xsrf="+getCookie("_xsrf")+"&username="+"sunck"...",
	  	 	function(data){   // 回调函数。
	  	 		alert("ok")
	  	 	})
	  	 }	
	  	 
	  	 2.function login(){
	  	 	data = {"username":"sunck","passwd":"123456"}
	  	 	var datastr = JSON.stringify(data)
	  	 	$.ajax({
	  	 		url:"/postfile",
	  	 		method:"POST",    // 可以GET
	  	 		data:datastr,
	  	 		success:function(data){
	  	 			alert("ok")
	  	 		}
	  	 		headers:{
	  	 			"X-XSRFToken":getCookie("_xsrf")
	  	 		}
	  	 	})
	  	 }


​ 在进入主页时就自动设置xsrf ,
​ 继承tornado.web.StaticFileHandler,
​ def init(self,args,**kw):
​ super().init(
args,**kw)
​ self.xsrf_token

用户验证:
tornado.web.authenticated装饰器, Tornado将确保合法用户才能用
验证:
def get_current_user(self):
验证失败,将重定向到配置文件中的"login_url":"/login"

class LoginHandler(RequestHandler):
    def get(self):
        next=self.get_argument("next","/login")
        url = "login?next="+next
        self.render("login.html",next=url)

class HomeHandler():
    def get_current_user(self):
        logined = self.get_cookie("logined",None)
        return logined
    @authenticated
    def get(self):
        self.write("welcome to home")

同步与异步:

tornado.httpclient.AsyncHTTPClient :
		 
fetch(request.callback=None) --> 用于执行一个web请求,并异步响应返回一个
	tornado.httpclient.HttpRespones
	request可以是一个 url,也可以是一个tornado.httpclient.HttpRequest

HttpRequest: HTTP请求类,该类的构造函数可以接收参数
			 参数: 

回调异步:

class StudentsHandler(RequestHandler):
	def on_respone(self,response):
		if response.error:
			self.send_error(500)
		else:
			data = json.loads(response.body)
			self.write(data)
			self.finish()   #关闭通道
	@tornado.web.asynchronous  #不关闭通信的通道。
	def get(self,*args):
		url = "http://www......"
		client = AsyncHTTPClient()
		client.fetch(url,self.on_response)
		self.write("ok")
		

协程异步:

@tornoda.gen.corountine
	def get(self, *args):
		url = "http://www......"
		client = AsyncHTTPClient()
		res = yield client.fetch(url)	--> self.getDate()
		if res.error:
			ret={"ret":0}
		else:
			ret = json.loads(res.body)
		self.write(ret)

Tornado的WebSocket模块:
open()
on_message(message)
on_close() 当WebSocket链接关闭后调用
write_message(message,binary=False) 主动向客户端发送message消息,字符串或字典(自动转json)
close() 关闭WebSocket链接
check_origin(origin) 判断源origin,对于符合条件的请求源允许连接。

blacksheep

大部分代码都是基于 Cython 编写的

pip install 'blacksheep[full]' uvicorn

简单程序

from blacksheep import Application
import uvicorn

# 和 FastAPI 一样,先创建一个 app
app = Application()

# 通过装饰器的方式注册路由
# 如果 methods 参数不指定,默认为 ["GET"],表示只接收 GET 请求
@app.route("/index", methods=["GET"])
async def index():
    return "Hello World"

# 在 Windows 中必须加上 if __name__ == "__main__"
# 否则会抛出 RuntimeError: This event loop is already running
if __name__ == "__main__":
    # 启动服务,因为我们这个文件叫做 main.py
    # 所以需要启动 main.py 里面的 app
    # 第一个参数 "main:app" 就表示这个含义
    # 然后是 host 和 port 表示监听的 ip 和端口
    uvicorn.run("main:app", host="0.0.0.0", port=5555)

blacksheep-cli

pip install blacksheep-cli

blacksheep create

注册路由

from blacksheep import Application

app = Application()

# 方式一
@app.route("/index", methods=["GET"])
async def index():
    pass

# 方式二
@app.router.get("/index")
async def index():
    pass

# 方式三
async def index():
    pass

app.router.add_get("/index", index)

# 方式四
async def index():
    pass

app.router.add("GET", "/index", index)

动态路径

@app.router.get("/items/{item_id}")
async def get_item(item_id: int):
    # 此时的 item_id 要求一个整型
    # 准确的说是一个能够转成整型的字符串
    return {"item_id": item_id}
路径包含/
# 声明 file_path 的类型为 path,这样它会被当成一个整体
# 但要注意:在 FastAPI 里面是 {file_path:path},这里刚好相反
@app.router.get("/files/{path:file_path}")
async def get_file(file_path: str):
    return {"file_path": file_path}
自定义匹配器
# 路由本质上就是一个 Route 对象
# 该对象有一个类属性 value_patterns,是一个字典
# 将自定义匹配器注册进去,然后就可以在路径参数里面使用了
Route.value_patterns["number_format"] = r"185\d{8}"

@app.router.get("/phone/{number_format:phone_number}")
async def get_file(phone_number: str):
    return {"phone_number": phone_number}

查询参数

from blacksheep import Application
import uvicorn

app = Application()

@app.router.get("/users/{user_id}")
async def get_user(user_id: str, name: str, age: int):
    """
    我们在函数中定义了 user_id、name、age 三个参数
    显然 user_id 和 路径参数中的 user_id 对应
    然后 name 和 age 会被解释成查询参数,没有默认值就是必传项
    这三个参数的顺序没有要求,但一般都是路径参数在前,查询参数在后
    """
    return {"user_id": user_id, "name": name, "age": age}

requests.get("http://localhost:5555/users/001?name=neo&age=17")

同一个查询参数出现多次

@app.router.get("/users")
async def get_user(name: List[str]):
    return {"name": name}

请求的载体:Request 对象

@app.router.get("/girl/{user_id}")
async def read_info(request: Request, user_id: str):
    """
    路径参数(user_id)其实可以不用定义,因为我们定义了 request: Request
    那么请求相关的所有信息都会进入到这个 Request 对象中
    
    当然定义了也无所谓,路径参数、查询参数可以和 Request 对象一起使用
    """
    # 路径参数(一个字典)
    path_params = request.route_values
    # 查询参数(一个字典)
    query_params = request.query

获取请求头

@app.router.get("/get_headers")
async def get_headers(request: Request):
    # 返回一个 Headers 对象,该对象是 Cython 实现的
    headers = request.headers
    print(headers.keys())
    """
    (b'host', b'user-agent', b'accept-encoding', b'accept', b'connection')
    """
    print(headers.values)
    """
    [(b'host', b'localhost:5555'), (b'user-agent', b'python-requests/2.28.1'), 
     (b'accept-encoding', b'gzip, deflate'), (b'accept', b'*/*'), 
     (b'connection', b'keep-alive')]
    """
    print(b"host" in headers)
    """
    True
    """
    # 获取的时候,key 一律小写,并且是字节串形式,并且返回的 value 也是字节串
    # 注意:headers.get_first 在 key 不存在的时候,会返回 None
    # 所以应该确保 key 存在之后,再进行 decode
    return {"User-Agent": headers.get_first(b"user-agent").decode("utf-8")}


# 获取cookie
cookies = request.cookies

async def get_extra_message(request: Request):
    return {
        "请求的主机": request.host,
        "请求方法": request.method,
        # request.url 返回一个 URL 对象,里面有如下属性
        # schema、host、port、path、query、fragment
        # 分别对应一个 URL 的不同部分
        "请求的 URL": str(request.url),
        "请求的 URL 的路径": request.path,
        "客户端 IP": request.client_ip,
    }

响应的载体:Response 对象

cdef class Response(Message):
    def __init__(self, int status, list headers = None, 
                 Content content = None)
    
# 该对象接收三个属性,分别是状态码、响应头和 Content 对象,而 Content 是一个普通的静态类
cdef class Content:
	cdef readonly bytes type
    cdef readonly bytes body
    cdef readonly int length
#该类包含三个属性,分别是响应类型、响应体、响应体的长度,但实际上第三个属性不需要传。
from blacksheep import (
    Application, Request,
    Response, Content,
    Cookie
)
import uvicorn
import orjson

app = Application()

@app.router.get("/get_info")
async def get_info(request: Request):
    data = {"name": "古明地觉", "age": 17}
    content = orjson.dumps(data)
    response = Response(
        200,
        [(b"ping", b"pong"), (b"token", b"0315abcde")],
        Content(b"application/json", content)
    )
    # 响应头也可以单独添加
    response.add_header(b"x-man", b"GOOD")
    """
    # 也可以移除某个响应头
    response.remove_header(b"name")
    """
    # 设置 cookie
    response.set_cookie(Cookie("session_id", "abcdefg"))
    # 也可以调用 set_cookies 同时设置多个 cookie,传一个列表即可
    """
    # 也可以移除某个 cookie
    response.remove_cookie("name")
    """
    return response

获取请求体

@app.router.post("/get_info")
async def get_info(request: Request):
    data = await request.read()
    print(data)
    
# 所以 await request.read() 得到的就是最原始的字节流,除了它之外还有 await request.json(),它在内部依旧会获取字节流,只不过获取之后会自动 loads 成字典

Form表单

# 调用 requests.post 发请求,如果参数通过 data 传递的话,则相当于提交了一个 form 表单
await request.form()

文件上传

# 返回一个列表,列表里面是 FormPart 对象
files = await request.files()
for file in files:
    result.append(
        {"name": file.name.decode("utf-8"),
         "file_name": file.file_name.decode("utf-8"),
         "content_type": file.content_type,
    )
requests.post(url, files=[('txt文件', open('...')), ...])

分块读取请求体

  • await request.read():读取原始的字节流
  • await request.form():将字节流按照表单的方式进行解析,返回一个字典
  • await request.json():将字节流按照 JSON 的方式进行解析,返回一个字典
@app.router.post("/stream")
async def stream(request: Request):
    buf = BytesIO()
    async for chunk in request.stream():
        buf.write(chunk)
    return buf.getvalue().decode("utf-8")

实现的效果和 await request.read() 是一样的,但当请求体很庞大时,该方式不会阻塞事件循环。

返回静态资源

# 浏览器输入:localhost:5555/static/images/1.png
# 会返回指定目录下的 1.png 文件
app.serve_files("/Users/satori/Downloads/images",
                root_path="static/images",
                discovery=True)

# 浏览器输入:localhost:5555/static/videos/1.mp4
# 会返回指定目录下的 1.mp4 文件
app.serve_files("/Users/satori/Downloads/videos",
                root_path="static/videos",
                discovery=True)

discover 参数,默认为 False。如果指定为 True,那么通过 /static/images 则可以查看对应目录的文件列表,

不同种类的响应

from blacksheep import text, html, json, pretty_json

# 返回纯文本
text("...")
# 返回 HTML
html("<h1>...</h1>")
# 返回 JSON
json({"k": "v"})
# 返回美化格式的 JSON
pretty_json({"k": "v"})
# 在 blacksheep 里面内置了非常多的函数,帮助我们构造不同种类的 Response
# 文件名 blacksheep/server/responses.py
# 里面的函数直接通过 from blcaksheep import 进行导入即可

def status_code(status: int = 200, message: Any = None) -> Response:
    if not message:
        return Response(status)
    return Response(status, content=_optional_content(message))

返回字节流

async def content():
    for i in range(1, 5):
        yield f"{i} chunk bytes ".encode("utf-8")

@app.router.get("/get_content")
async def get_content(request: Request):
    data = b"".join([chunk async for chunk in content()])
    return Response(
        200, None,
        Content(b"application/octet-stream", data)
    )

返回文件

# 我们可以返回图片、音频、视频,以字节流的形式
# 但光有字节流还不够,我们还要告诉 Chrome
# 拿到这坨字节流之后,应该要如何解析
# 此时需要通过响应头里面的 Content-Type 指定
Response(
    200, None,
    # png 图片:"image/png"
    # mp3 音频:"audio/mp3"
    # mp4 视频:"video/mp4"
    Content(b"image/png", b"*** bytes data ***")
)

直接下载

   # 在响应头中指定 Content-Disposition
    # 意思就是告诉 Chrome,你不要解析了,直接下载下来
    # filename 后面跟的就是文件下载之后的文件名
    return Response(
        # 既然都下载下来了,也就不需要 Chrome 解析了
        200, [(b"Content-Disposition", b"attachment; filename=main.py")],
        # 将响应类型指定为 application/octet-stream
        # 表示让 Chrome 以二进制格式直接下载
        Content(b"application/octet-stream", data)
    )

中间件

中间件在 web 开发中可以说是非常常见了,说白了中间件就是一个函数或者一个类。

在请求进入视图函数之前,会先经过中间件(被称为请求中间件),在里面我们可以对请求进行一些预处理,或者实现一个拦截器等等;同理当视图函数返回响应之后,也会经过中间件(被称为响应中间件),在里面我们也可以对响应进行一些润色。

blacksheep 也支持像 Flask 一样自定义中间件,在 Flask 里面有请求中间件和响应中间件,但在 blacksheep 里面这两者合二为一了,

async def middleware(request: Request, handler):
    # 请求到来时会先经过这里的中间件
    if request.headers.get("ping", "") != "pong":
        result = orjson.dumps({"error": "请求头中缺少指定字段"})
        response = Response(
            200, None,
            Content(b"application/json", result)
        )
        # 当请求头中缺少 "ping": "pong"
        # 在中间件这一步就直接返回了,就不会再往下走了
        # 所以此时相当于实现了一个拦截器
        return response
    
    # 如果条件满足,则执行await handler(request),关键是这里的 handler
    # 如果该中间件后面还有中间件,那么 handler 就是下一个中间件
    # 如果没有,那么 handler 就是对应的视图函数
    # 这里显然是视图函数,因此执行之后会拿到视图函数返回的 Response 对象
    response: Response = await handler(request)
    # 我们对 response 做一些润色,比如设置一个响应头
    # 所以我们看到在 blacksheep 中,请求中间件和响应中间件合在一起了
    response.set_header(b"status", b"success")
    return response

# 将中间件添加进去
app.middlewares.append(middleware)

作用与单个请求

from blacksheep.server.normalization import ensure_response
def add_response_header(headers: Dict[str, str]):
    def decorator(handler):
        @wraps(handler)
        async def wrapped(*args, **kwargs):
            # 注意:此处拿到的就是视图函数的返回值
            # 如果视图函数返回的不是 Response 对象,那么这里不会自动包装
            # 因此调用 ensure_response,将其转成 Response 对象
            response = ensure_response(await handler(*args, **kwargs))
            for key, val in headers.items():
                response.set_header(key.encode("utf-8"), val.encode("utf-8"))
            return response
        return wrapped
    return decorator

@app.router.get("/")
@add_response_header({"Token": "abcdefg"})
# 这里的 view_func 就是 add_response_header 里面的 wrapped
async def view_func(request: Request):
    
通过装饰器的方式实现了响应头的添加,虽然它不是中间件,但在最小范围内实现了中间件的效果。

websocket

注意:使用 WebSocket 功能之前需要安装相关的库,直接 pip install websockets。

from blacksheep import Application, WebSocket
@app.router.ws("/ws/{client_id}")
async def ws(websocket: WebSocket, client_id: str):
    # accept 方法接收两个参数:headers 和 subprotocol
    # headers 表示响应头,subprotocol 表示应用程序接受的子协议
    # 它们将和握手响应一起发送给客户端
    await websocket.accept()  # 这里不需要指定

# 或者也可以这么注册
"""
app.router.add_ws("/ws/{client_id}", ws)
# add_ws 方法本质上也是调用了 add 方法
app.router.add("GET_WS", "/ws/{client_id}", ws)
"""

一旦连接建立成功,那么就可以和客户端收发消息了,方法有以下几种:

  • receive_bytes 和 send_bytes
  • receive_text 和 send_text
  • receive_json 和 send_json
await websocket.accept()
await websocket.send_text(f"客户端{client_id} 来连接啦")
while True:
    msg = await websocket.receive_text()
    await websocket.send_text(f"收到客户端发送的消息: {msg}")

认证(Authentication)

使用Session验证

解决多台服务器session共享

  1. session复制

  2. session粘黏

    Nginx 的 sticky 模块可以支持这种方式,支持按 ip 或 cookie 粘连等等,比如按 ip 粘连

  3. session共享

    redis

使用Token认证

这个 Token 必须要在每次请求时都传递给服务端,它应该保存在请求头里。 如果保存在 Cookie 里面,服务端还要支持 CORS(跨来源资源共享)策略

而基于 Token 认证一般采用 JWT,它由三段信息构成,将这三段信息用 . 连接起来就构成了 JWT 字符串。就像如下这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

第一部分我们称之为头部(Header)、第二部分我们称之为载荷(Payload)、第三部分我们称之为签名(Signature),下面来分别介绍

总结:Token 解决什么问题(为什么要用 Token)?

  • 1)完全由应用管理,可以避开同源策略;
  • 2)支持跨域访问,Cookie 不支持。Cookie 跨站是不能共享的,这样的话如果你要实现多应用(多系统)的单点登录(SSO),使用 Cookie 来做的话就很困难了。但如果用 Token 来实现 SSO 会非常简单,只要在 header 中的 Authorize 字段(或其他自定义)加上 Token 即可完成所有跨域站点的认证;
  • 3)Token 是无状态的,可以在多个服务器间共享,因为服务端不需要保存 Token,只需要对它进行解析即可;
  • 4)Token 可以避免 CSRF 攻击(跨站请求攻击);
  • 5)易于扩展,在移动端的原生请求是没有 Cookie 之说的,而 Sessionid 依赖于 Cookie,所以 SessionID 就不能用 Cookie 来传了。如果用 Token 的话,由于它是随着 header 的 Authorization 传过来的,也就不存在此问题,换句话说 Token 天生支持移动平台,可扩展性好;

解决跨域问题

app = Application()

app.use_cors(
    # 允许跨域请求的 HTTP 方法列表
    # ["*"] 表示支持所有请求
    allow_methods=["GET", "POST"],
    # 允许跨域请求的 HTTP 请求头列表
    # 可以使用 ["*"] 表示允许所有的请求头
    # 当然下面几个请求头总是被允许的
    # Accept、Accept-Language、Content-Language、Content-Type
    allow_headers=["*"],
    # 允许跨域的源列表,例如 ["http://localhost:8080"]
    # ["*"] 表示允许任何源
    allow_origins=["*"],
    # 跨域请求是否支持 cookie,默认是 False
    # 如果为 True,allow_origins 必须为具体的源,不可以是 ["*"]
    allow_credentials=False,
    # 可以被浏览器访问的响应头, 一般很少指定
    expose_headers=["*"],
    # 设定浏览器缓存 CORS 响应的最长时间,单位是秒,一般也很少指定
    max_age=1000
)

# 但有些请求访问的资源非常重要,我们需要做一些限制
# 创建一个新的 CORS 规则,名称为 allow_some
app.add_cors_policy(
    "allow_some",
    # 只允许 POST 请求
    allow_methods=["POST"],
    # 源必须是 http://127.0.0.1
    allow_origins=["http://127.0.0.1"],
)

# 规则可以创建多个,如果 allow_* 相关参数不指定
# 那么表示拒绝所有请求
app.add_cors_policy(
    "deny"
)

@app.cors("allow_some")
@app.router.post("/payment")
async def payment():
    return "支付相关, 很重要的接口"

JWT

ALGORITHM = 'HS256'
SECRET = 'somepassword'
import jwt
def jwt_b_decode(st):
    try:
        return_data = jwt.decode(st, SECRET, algorithms=[ALGORITHM])
    except Exception as e:
        logging.exception(e)
        return_data = str(e)
    return return_data
    

挂载 Application 对象

类似flask蓝图

# views/__init__.py
from .products import app as products_app
from .users import app as users_app

# main.py
from views import (
	products_app,
    users_app
)

app = Application()
app.mount("/products", products_app)
app.mount("/users", users_app)

#如果是在子 app 中就需要注意了,子 app 创建路由时的视图函数只能接收一个 Request 对象,然后返回一个 Response 对象。

# 将该属性设置为 True 即可
# 此时参数和返回值不再受到限制,所有的视图函数的处理逻辑都一样
app.mount_registry.auto_events = True
app.mount("/", child)

依赖注入

class GenerateRandom:

    def __init__(self):
        self.number = random.randint(1, 100)

@app.router.get("/get_random")
async def index(request: Request,
                gn: GenerateRandom):
    return {"number": gn.number}

# Application 对象公开了一个 services 属性,可以将类型注册为服务
# 当视图函数的参数签名引用了注册的类型时,该类型的实例会在调用视图函数时自动注入
app.services.add_exact_scoped(GenerateRandom)

# 这样只有在第一次请求的时候实例化,后续就一直使用第一次创建的实例
app.services.add_exact_singleton(GenerateRandom)

# 直接添加一个实例进去, 可以加参数, 也是单例的
app.services.add_instance(GenerateRandom(1, 100))
app.services.add_transient(A)   # 每次调用生成一个实例
app.services.add_scoped(B) 		# 每个请求生成一个实例
app.services.add_singleton(C)   # 所有请求使用一个实例

数据库连接池

from sqlalchemy.ext.asyncio import (
    create_async_engine, AsyncEngine
)
from sqlalchemy.engine.url import URL
from sqlalchemy import text
# 创建一个数据库引擎,它一般会定义在单独的文件中
def create_database_engine():
    engine = create_async_engine(
        URL.create("mysql+asyncmy", username="root", password="123456",
                   host="82.157.146.194", port=3306, database="mysql")
    )
    return engine

@app.router.get("/get_data")
async def index(request: Request, engine: AsyncEngine):
    # sqlalchemy 中的异步引擎的类型是 AsyncEngine
    # 将它注册在 services 中,这里就可以直接用了
    query = text("SELECT name, age, address FROM girl")
    # 引擎维护了一个连接池,通过 async with 取出一个连接
    # 执行完毕之后,再放回连接池
    async with engine.connect() as conn:
        rows = await conn.execute(query)
    # 获取返回的字段名
    columns = rows.keys()
    # 遍历 rows,将字段名和每一行数据拼接成字典,然后返回
    results = [dict(zip(columns, row)) for row in rows]
    return results

app.services.add_instance(create_database_engine())

像数据库连接这种,它应该在应用程序启动的时候就创建好,所以 blacksheep 提供了两个触发器列表,分别表示在程序启动和关闭时执行的一系列操作。

async def create_database_engine(app: Application):
    print("我要创建引擎啦")
    engine = create_async_engine(
        URL.create("mysql+asyncmy", username="root", password="123456",
                   host="82.157.146.194", port=3306, database="mysql")
    )
    # 将引擎注册进去
    app.services.add_instance(engine)
    
async def dispose_database_engine(app: Application):
    # 通过 app.services.add_instance 将实例注册进去之后
    # 在视图函数中只要指定好类型,那么就会自动取出来,这没有问题
    # 但如果不在视图函数中呢?所以 app 提供了一个 service_provider
    # 注册在 services 里面的实例,都会保存在 service_provider 中
    
    # 我们直接像访问字典一样获取实例即可,那么问题来了,key 是啥?
    # 很简单,我们在注册实例(假设叫 obj)的时候,会执行以下操作
    """
    app.service_provider[type(obj)] = obj
    app.service_provider[type(obj).__name__] = obj
    app.service_provider[type(obj).__name__.lower()] = obj
    """
    engine: AsyncEngine
    # 所以可以这么获取
    engine = app.services.provider[AsyncEngine]
    engine = app.services.provider["AsyncEngine"]
    engine = app.services.provider["asyncengine"]
    # 以上几个 engine 都指向同一个对象
    # 最后,如果是驼峰命名法,那么还可以这么获取
    engine = app.service_provider["async_engine"]
    # 方式比较多,我们以类型作为 key 去获取实例即可
    
    # 关闭引擎,并关闭底层连接池
    await engine.dispose()
    print("引擎关闭啦")
 
# app.on_start 和 app.on_event 都是 ApplicationEvent 实例
# 前者保存了程序启动时要执行逻辑,后者保存了程序关闭时要执行的逻辑
app.on_start += create_database_engine
app.on_stop += dispose_database_engine

SQLAlchemy

from blacksheepsqlalchemy import use_sqlalchemy

if settings.db_connection_string:
	use_sqlalchemy(app, connection_string="sqlite+aiosqlite:///torino.db")
from sqlalchemy.ext.asyncio import AsyncSession
@post("/user")
async def create_user(req: FromJSON[CreateUser], session: AsyncSession):
    data = req.value
    async with session:
        session.add()
        await session.commit()
        
# 查询
stmt = select(User).where(User.username == 'example')
# 执行查询
result = await session.execute(stmt)
# 获取查询结果
user = result.scalars().first()

交互式文档

docs = OpenAPIHandler(
    info=Info(title="Example API", version="0.0.1"),
    # 以下几个参数都是默认的,可以自由调整
    # ui_path="/docs",
    # json_spec_path="/openapi.json",
    # yaml_spec_path="/openapi.yaml",
    # preferred_format=Format.JSON,
    # anonymous_access=True
)
docs.bind_app(app)

Application 对象

# 开启错误详细信息
app = Application(show_error_details=True)

异常处理

class CustomException(Exception):
    pass

async def custom_exception_handler(
        self, request: Request, exc: CustomException
) -> Response:
    errors = orjson.dumps({"error": "出现了 CustomException",
                           "exc": str(exc)})
    return Response(404, None, Content(b"application/json", errors))

# 如果视图函数执行出现 IndexError,那么就去执行 indexerror_handler
# 里面的 self 是 app,request 就是本次请求对应的 Request 对象,exc 就是异常
app.exceptions_handlers[IndexError] = indexerror_handler
# 如果视图函数执行出现 CustomException,那么就去执行 custom_exception_handler
app.exceptions_handlers[CustomException] = custom_exception_handler

# 将异常和 handler 进行绑定还可以使用装饰器的方式
@app.exception_handler(IndexError)
async def indexerror_handler(
        self, request: Request, exc: IndexError
) -> Response:
    errors = orjson.dumps({"error": "出现了 IndexError",
                           "exc": str(exc),
                           "index": request.query["index"][0]})
    return Response(404, None, Content(b"application/json", errors))

Application 对象还暴露了三种事件:分别是 on_start、after_start、on_stop

img

@app.after_start
async def after_start_print_routes(app: Application) -> None:
    print(app.router.routes)

uvicorn 部署 blacksheep 服务

import uvicorn
if __name__ == '__main__':
    uvicorn.run("main:app", port=5555)

还可以通过命令行的方式启动

uvicorn main:app --port 5555 --reload 

如果是通过命令行的方式启动,那么参数要将参数中的 _ 替换成 -,比如 reload_dirs 要改成 --reload-dirs

posted @ 2025-06-09 20:33  少侠来也  阅读(48)  评论(0)    收藏  举报