环境

CentOS Linux release 7.5.1804

Python 3.6.4/2.7.14

简介

Airflow 是 Airbnb 开源的一个用 Python 编写的工作流管理平台,自带 web UI 和调度,目前在Apache下做孵化。

Airflow 管理页面

Airflow 中有两个基本概念,DAG和task。
DAG是多个task的集合,定义在一个Python文件中,包含了task之间的依赖关系,如task A在task B之后执行,task C可以单独执行等等。

安装并运行

# 默认目录在~/airflow,也可以使用以下命令来指定目录
export AIRFLOW_HOME=~/airflow

pip install apache-airflow

# 初始化数据库
airflow initdb

# 启动web服务,默认端口为8080,也可以通过`-p`来指定
airflow webserver -p 8080

# 启动 scheduler
airflow scheduler

定义第一个DAG

$AIRFLOW_HOME目录下新建dags文件夹,后面的所有dag文件都要存储在这个目录。

新建dag文件hello_world.py,语句含义见注释

# coding: utf-8

from airflow import DAG
from airflow.operators.python_operator import PythonOperator
from datetime import datetime, timedelta


# 定义默认参数
default_args = {
    'owner': 'airflow',  # 拥有者名称
    'start_date': datetime(2018, 6, 6, 20, 00),  # 第一次开始执行的时间,为格林威治时间,为了方便测试,一般设置为当前时间减去执行周期
    'email': ['shelmingsong@gmail.com'],  # 接收通知的email列表
    'email_on_failure': True,  # 是否在任务执行失败时接收邮件
    'email_on_retry': True,  # 是否在任务重试时接收邮件
    'retries': 3,  # 失败重试次数
    'retry_delay': timedelta(seconds=5)  # 失败重试间隔
}

# 定义DAG
dag = DAG(
    dag_id='hello_world',  # dag_id
    default_args=default_args,  # 指定默认参数
    # schedule_interval="00, *, *, *, *"  # 执行周期,依次是分,时,天,月,年,此处表示每个整点执行
    schedule_interval=timedelta(minutes=1)  # 执行周期,表示每分钟执行一次
)


# 定义要执行的Python函数1
def hello_world_1():
    current_time = str(datetime.today())
    with open('/root/tmp/hello_world_1.txt', 'a') as f:
        f.write('%s\n' % current_time)
    assert 1 == 1  # 可以在函数中使用assert断言来判断执行是否正常,也可以直接抛出异常


# 定义要执行的Python函数2
def hello_world_2():
    current_time = str(datetime.today())
    with open('/root/tmp/hello_world_2.txt', 'a') as f:
        f.write('%s\n' % current_time)


# 定义要执行的Python函数3
def hello_world_3():
    current_time = str(datetime.today())
    with open('/root/tmp/hello_world_3.txt', 'a') as f:
        f.write('%s\n' % current_time)

# 定义要执行的task 1
t1 = PythonOperator(
    task_id='hello_world_1',  # task_id
    python_callable=hello_world_1,  # 指定要执行的函数
    dag=dag,  # 指定归属的dag
    retries=2,  # 重写失败重试次数,如果不写,则默认使用dag类中指定的default_args中的设置
)

# 定义要执行的task 2
t2 = PythonOperator(
    task_id='hello_world_2',  # task_id
    python_callable=hello_world_2,  # 指定要执行的函数
    dag=dag,  # 指定归属的dag
)

# 定义要执行的task 3
t3 = PythonOperator(
    task_id='hello_world_3',  # task_id
    python_callable=hello_world_3,  # 指定要执行的函数
    dag=dag,  # 指定归属的dag
)

t2.set_upstream(t1)
# 表示t2这个任务只有在t1这个任务执行成功时才执行,
# 等价于 t1.set_downstream(t2)
# 同时等价于 dag.set_dependency('hello_world_1', 'hello_world_2')

t3.set_upstream(t1)  # 同理

写完后执行它检查是否有错误,如果命令行没有报错,就表示没问题。

python $AIRFLOW_HOME/dags/hello_world.py

通过以下命令查看生效的dags

[root@localhost dags]# airflow list_dags
[2018-06-06 21:03:25,808] {__init__.py:45} INFO - Using executor SequentialExecutor
[2018-06-06 21:03:25,877] {models.py:189} INFO - Filling up the DagBag from /root/airflow/dags


-------------------------------------------------------------------
DAGS
-------------------------------------------------------------------
hello_world

查看hello_world这个dag下面的tasks

[root@localhost dags]# airflow list_tasks hello_world
[2018-06-06 21:04:45,736] {__init__.py:45} INFO - Using executor SequentialExecutor
[2018-06-06 21:04:45,805] {models.py:189} INFO - Filling up the DagBag from /root/airflow/dags
hello_world_1
hello_world_2
hello_world_3

查看hello_world这个dag下面tasks的层级关系

[root@localhost dags]# airflow list_tasks hello_world --tree
[2018-06-06 21:05:42,956] {__init__.py:45} INFO - Using executor SequentialExecutor
[2018-06-06 21:05:43,020] {models.py:189} INFO - Filling up the DagBag from /root/airflow/dags
<Task(PythonOperator): hello_world_2>
    <Task(PythonOperator): hello_world_1>
<Task(PythonOperator): hello_world_3>
    <Task(PythonOperator): hello_world_1>

如果按照以上步骤启动了schedule,则DAG已经开始定时执行了,我们设置了每分钟执行一次,可以访问your_domain:8080来查看任务的执行情况。
也可以查看/root/tmp/hello_world_1.txt/root/tmp/hello_world_2.txt/root/tmp/hello_world_3.txt文件中的内容来检查任务是否执行成功。

执行失败时email通知

如果需要在任务执行失败(执行过程中有异常抛出)的时候邮件通知,除了在DAG文件中指定接收email列表外,还需要在配置文件中指定发送邮箱的信息,打开配置文件$AIRFLOW_HOME/airflow.cfg,修改以下配置项,修改完需要重启webserver和schedule

smtp_host = smtp.163.com  # smtp邮箱地址
smtp_starttls = True  # 是否tls加密
smtp_mail_from = demo@163.com  # 发件人邮箱地址,需开通smtp服务
smtp_ssl = False  # 是否ssl加密
smtp_port = 25  # smtp端口号

使用位位移来指定执行顺序

以下四行的作用是相同的

op1 >> op2
op1.set_downstream(op2)

op2 << op1
op2.set_upstream(op1)

也可以连续使用位位移

op1 >> op2 >> op3 << op4

以上等价于

op1.set_downstream(op2)
op2.set_downstream(op3)
op3.set_upstream(op4)

使用变量(Variables)

变量的value可以在UI界面的Admin > Variables里面进行增删改查

可以在代码中这样使用变量

from airflow.models import Variable
foo = Variable.get("foo", default_var='a')  # 设置当获取不到时使用的默认值
bar = Variable.get("bar", deserialize_json=True)  # 对json数据进行反序列化

更多

Airflow doc

博客更新地址

环境

CentOS Linux release 7.5.1804

脚本完成操作

  • 切换CentOS软件镜像源为中科大软件源
  • 设置防火墙,允许全部端口通过
  • 安装git
  • 安装Pyenv包管理工具,Pyenv使用详见:Python版本管理工具 Pyenv的安装与使用
  • 安装Python 3.6.4
  • 切换pip源为豆瓣
  • 安装Docker
  • 安装MySQL
  • 初始化MySQL(root@localhost的密码为123456,song@%的密码为123456)
  • 设置sshd开机启动
  • 安装ntp时间自动更新工具

使用

  • Github下载部署脚本代码
  • 百度云下载包含MySQL rpm包的required_rpms文件夹(MySQL官网由于国内特殊的网络环境原因,下载很慢,经常中断导致无法安装,因而将安装所需的rpm包单独down了下来,又因为所有MySQL包加起来有200+M,不方便传到Github上,只能传到百度云上单独下载)
  • 将百度云上下载的required_rpms文件夹加入到代码主目录,最终目录如下:
.
├── 0_start.sh
├── 1_shell_init.sh
├── 2_deploy_firewall.sh
├── 3_install_git.sh
├── 4_install_pip.sh
├── 5_install_docker.sh
├── 6_install_mysql.sh
├── 7_enable_sshd.sh
├── 99_shell_finalize.sh
├── deploy_mysql_hook
│   ├── 0_deploy_mysql.sh
│   └── init_mysql.exp
├── post_hook
│   └── 1_test.sh
├── pre_hook
│   └── 1_test.sh
├── README.md
└── required_rpms
    ├── mysql
    │   ├── mariadb-5.5.56-2.el7.x86_64.rpm
    │   ├── perl-5.16.3-292.el7.x86_64.rpm
    │   ├── perl-Carp-1.26-244.el7.noarch.rpm
    │   ├── perl-constant-1.27-2.el7.noarch.rpm
    │   ├── perl-Encode-2.51-7.el7.x86_64.rpm
    │   ├── perl-Exporter-5.68-3.el7.noarch.rpm
    │   ├── perl-File-Path-2.09-2.el7.noarch.rpm
    │   ├── perl-File-Temp-0.23.01-3.el7.noarch.rpm
    │   ├── perl-Filter-1.49-3.el7.x86_64.rpm
    │   ├── perl-Getopt-Long-2.40-2.el7.noarch.rpm
    │   ├── perl-HTTP-Tiny-0.033-3.el7.noarch.rpm
    │   ├── perl-libs-5.16.3-292.el7.x86_64.rpm
    │   ├── perl-macros-5.16.3-292.el7.x86_64.rpm
    │   ├── perl-parent-0.225-244.el7.noarch.rpm
    │   ├── perl-PathTools-3.40-5.el7.x86_64.rpm
    │   ├── perl-Pod-Escapes-1.04-292.el7.noarch.rpm
    │   ├── perl-podlators-2.5.1-3.el7.noarch.rpm
    │   ├── perl-Pod-Perldoc-3.20-4.el7.noarch.rpm
    │   ├── perl-Pod-Simple-3.28-4.el7.noarch.rpm
    │   ├── perl-Pod-Usage-1.63-3.el7.noarch.rpm
    │   ├── perl-Scalar-List-Utils-1.27-248.el7.x86_64.rpm
    │   ├── perl-Socket-2.010-4.el7.x86_64.rpm
    │   ├── perl-Storable-2.45-3.el7.x86_64.rpm
    │   ├── perl-Text-ParseWords-3.29-4.el7.noarch.rpm
    │   ├── perl-threads-1.87-4.el7.x86_64.rpm
    │   ├── perl-threads-shared-1.43-6.el7.x86_64.rpm
    │   ├── perl-Time-HiRes-1.9725-3.el7.x86_64.rpm
    │   └── perl-Time-Local-1.2300-2.el7.noarch.rpm
    ├── mysql57-community-release-el7-11.noarch.rpm
    ├── mysql-community-server
    │   ├── mysql-community-client-5.7.20-1.el7.x86_64.rpm
    │   ├── mysql-community-common-5.7.20-1.el7.x86_64.rpm
    │   ├── mysql-community-devel-5.7.20-1.el7.x86_64.rpm
    │   ├── mysql-community-libs-5.7.20-1.el7.x86_64.rpm
    │   ├── mysql-community-libs-compat-5.7.20-1.el7.x86_64.rpm
    │   └── mysql-community-server-5.7.20-1.el7.x86_64.rpm
    └── mysql-devel
        ├── keyutils-libs-devel-1.5.8-3.el7.x86_64.rpm
        ├── krb5-devel-1.15.1-8.el7.x86_64.rpm
        ├── libcom_err-devel-1.42.9-10.el7.x86_64.rpm
        ├── libkadm5-1.15.1-8.el7.x86_64.rpm
        ├── libselinux-devel-2.5-11.el7.x86_64.rpm
        ├── libsepol-devel-2.5-6.el7.x86_64.rpm
        ├── libverto-devel-0.2.5-4.el7.x86_64.rpm
        ├── mariadb-devel-5.5.56-2.el7.x86_64.rpm
        ├── openssl-devel-1.0.2k-8.el7.x86_64.rpm
        ├── pcre-devel-8.32-17.el7.x86_64.rpm
        └── zlib-devel-1.2.7-17.el7.x86_64.rpm
  • 在主目录运行source 0_start.sh,等待执行完成即可,执行结果如下:
==================================================
shell exec time
1_shell_init.sh      exec time:      0 m  25 s
pre_hook     exec time:      0 m  0 s
2_deploy_firewall.sh     exec time:      0 m  2 s
3_install_git.sh     exec time:      0 m  7 s
4_install_pip.sh     exec time:      4 m  3 s
5_install_docker.sh      exec time:      1 m  2 s
6_install_mysql.sh   exec time:      1 m  6 s
deploy_mysql_hook    exec time:      0 m  3 s
7_enable_sshd.sh     exec time:      0 m  0 s
post_hook    exec time:      0 m  0 s

all_shell        exec time:      6 m  48 s
==================================================

博客更新地址

环境

Ubuntu 16.04 桌面版

安装

使用或调试pyautogui程序需要在有图形界面的系统上,此处使用的是 Ubuntu 16.04 桌面版。

pip install python3–xlib
apt–get install scrot
apt–get install python3–tk
apt–get install python3–dev
pip install pyautogui

鼠标操作

鼠标移动

pyautogui 使用 x-y 坐标系,左上角的坐标是(0, 0)

获取屏幕分辨率

screen_width, screen_height = pyautogui.size()

获取当前鼠标坐标

x, y = pyautogui.position()

使用绝对坐标移动(moveTo)

顺时针移动,画十次方框,duration参数表示持续时间,即当前点到达下一个点所需花费的时间。

for i in range(10):
    pyautogui.moveTo(300, 300, duration=0.25)
    pyautogui.moveTo(400, 300, duration=0.25)
    pyautogui.moveTo(400, 400, duration=0.25)
    pyautogui.moveTo(300, 400, duration=0.25)

画十次圆

screen_width, screen_height = pyautogui.size()
print('screen_width: %s\nscreen_height: %s' % (screen_width, screen_height))

r = 250 # 圆半径
o_x = screen_width / 2 # 圆心X坐标
o_y = screen_height / 2 # 圆心Y坐标
pi = math.pi

for i in range(10):
    for angle in range(0, 360, 20):
# 利用圆的参数方程
        x = o_x + r * math.sin(angle * pi / 180)
        y = o_y + r * math.cos(angle * pi / 180)
pyautogui.moveTo(x, y, duration=0.1)

使用相对坐标移动(moveRel)

for i in range(10):
    pyautogui.moveRel(100, 0, duration=0.25)
    pyautogui.moveRel(0, 100, duration=0.25)
    pyautogui.moveRel(-100, 0, duration=0.25)
    pyautogui.moveRel(0, -100, duration=0.25)

实时获取鼠标位置坐标

try:
    while True:
        x, y = pyautogui.position()
        print(x, y)
except KeyboardInterrupt:
    print('\nExit.')

鼠标点击

pyautogui.click(x=cur_x, y=cur_y, button='left')
  • x, y是要点击的位置,默认是鼠标当前位置
  • button 是要点击的按键,有三个可选值: left, middle, right,默认是left

在当前位置点击右键

pyautogui.click(button='right')

在指定位置点击左键

pyautogui.click(100, 100)

其它函数
* pyautogui.doubleClick(): 双击
* pyautogui.rightClick(): 右击
* pyautogui.middleClick(): 中击

鼠标拖拽

使用绝对坐标拖拽(dragTo)

# 画一个回字
pyautogui.moveTo(300, 300, duration=0.25)
pyautogui.dragTo(400, 300, duration=0.25)
pyautogui.dragTo(400, 400, duration=0.25)
pyautogui.dragTo(300, 400, duration=0.25)
pyautogui.dragTo(300, 300, duration=0.25)

pyautogui.moveTo(200, 200, duration=0.25)
pyautogui.dragTo(500, 200, duration=0.25)
pyautogui.dragTo(500, 500, duration=0.25)
pyautogui.dragTo(200, 500, duration=0.25)
pyautogui.dragTo(200, 200, duration=0.25)

使用相对坐标拖拽(dragRel)

# 画一个回字
pyautogui.moveTo(300, 300, duration=0.25)
pyautogui.dragRel(100, 0, duration=0.25)
pyautogui.dragRel(0, 100, duration=0.25)
pyautogui.dragRel(-100, 0, duration=0.25)
pyautogui.dragRel(0, -100, duration=0.25)

pyautogui.moveTo(200, 200, duration=0.25)
pyautogui.dragRel(300, 0, duration=0.25)
pyautogui.dragRel(0, 300, duration=0.25)
pyautogui.dragRel(-300, 0, duration=0.25)
pyautogui.dragRel(0, -300, duration=0.25)

滚轮

使用函数scroll(),它只接受一个整数,值为正则往上滚,值为负则往下滚。

pyautogui.scroll(200)

根据像素颜色定位按钮位置

img = pyautogui.screenshot()  # 截屏
img_color = img.getpixel((300, 300))  # 获取坐标颜色 (48, 10, 36)
is_matched = pyautogui.pixelMatchesColor(300, 300, (48, 10,36))  # 判断屏幕坐标的像素颜色是不是等于某个值
print(is_matched)  # True

根据按钮图片定位按钮位置

corn_locate = pyautogui.locateOnScreen('corn/settings.png') # 找到按钮所在坐标,分别含义是按钮左上角x坐标,左上角y坐标,x方向大小,y方向大小 (5, 560, 54, 54)
corn_center_x, corn_center_y = pyautogui.center(corn_locate) # 找到按钮中心点
pyautogui.click(corn_center_x, corn_center_y) # 点击按钮

键盘操作

键盘按键

输入普通字符串

pyautogui.typewrite('Hello, world!', 0.25)  # 0.25表示每输完一个字符串延时0.25秒

输入特殊字符

代码 按键
enter/return/\n 回车
esc ESC键
shiftleft/shiftright 左右SHIFT键
altleft/altright 左右ALT键
ctrlleft/ctrlright 左右CTRL键
tab/\t TAB键
backspace/delete BACKSPACE/DELETE键
pageup/pagedown PAGE UP/PAGE DOWN键
home/end HOME/END
up/down/left/right 上下左右键
f1/f2/… F1/F2/…
volumemute/volumedown/volumeup ??
pause PAUSE键
capslock/numlock/scrolllock CAPS LOCK/NUM LOCL/SCROLL LOCK键
insert INSERT键
printscreen PRINT SCREEN键
winleft/winright 左右Win键
command Mac上的command键
pyautogui.click(100, 100)
pyautogui.typewrite('Hello, world!', 0.25)
pyautogui.typewrite(['enter', 'a', 'b', 'left', 'left', 'X', 'Y'], '0.1')

键盘的按下和释放

  • keyDown(): 按下某个键
  • keyUp(): 松开某个键
  • press(): 一次完整的按键,前面两个函数的结合

如关闭某个窗口(ALT + F4)

pyautogui.keyDown('altleft')
pyautogui.press('f4')
pyautogui.keyUp('altleft')

或者直接使用热键函数

pyautogui.hotkey('altleft', 'f4')

博客更新地址

环境

Python 3.6.4

简介

Blinker是一个基于Python的强大的信号库,支持一对一、一对多的订阅发布模式,支持发送任意大小的数据等等,且线程安全。

安装

pip install blinker

使用

signal为单例模式

signal 使用了单例模式,允许代码的不同模块得到相同的signal,而不用互相传参。

In [1]: from blinker import signal

In [2]: a = signal('signal_test')

In [3]: b = signal('signal_test')

In [4]: a is b
Out[4]: True

订阅信号

使用.connect(func)方法来订阅一个信号,当信号发布时,该信号的订阅者会执行func

In [5]: def subscriber(sender):
   ...:     print('Got a signal sent by {}'.format(sender))
   ...:     

In [6]: ready = signal('ready')

In [7]: ready.connect(subscriber)
Out[7]: <function __main__.subscriber(sender)>

发布信号

使用.send()方法来发布信号,会通知所有订阅者,如果没有订阅者则什么都不会发生。

In [12]: class Processor(object):
    ...:     
    ...:     def __init__(self, name):
    ...:         self.name = name
    ...:         
    ...:     def go(self):
    ...:         ready = signal('ready') 
    ...:         ready.send(self)
    ...:         print('Processing...')
    ...:         complete = signal('complete')
    ...:         complete.send(self)
    ...:         
    ...:     def __repr__(self):
    ...:         return '<Processor {}>'.format(self.name)
    ...:     

In [13]: processor_a = Processor('a')

In [14]: processor_a.go()
Got a signal sent by <Processor a>
Processing...

订阅指定的发布者

.connect()方法接收一个可选参数sender,可用于接收指定发布者的信号。

In [18]: def b_subscriber():
    ...:     print('Caught signal from peocessor_b')
    ...:     

In [19]: ready.connect(b_subscriber, sender=processor_b)
Out[19]: <function __main__.b_subscriber(sender)>

In [20]: processor_a.go()
Got a signal sent by <Processor a>
Processing...

In [21]: processor_b.go()
Got a signal sent by <Processor b>
Caught signal from peocessor_b
Processing...

订阅者接收发布者传递的数据

除了之前的通过.connect方法来订阅外,还可以通过装饰器的方法来订阅。
订阅的方法可以接收发布者传递的数据。

In [22]: send_data = signal('send-data')

In [23]: @send_data.connect
    ...: def receive_data(sender, **kw):
    ...:     print('Caught signal from {}, data: {}'.format(sender, kw))
    ...:     return 'received!'
    ...: 
    ...: 

In [24]: result = send_data.send('anonymous', abc=123)
Caught signal from anonymous, data: {'abc': 123}

.send方法的返回值是一个由元组组成的列表,每个元组的第一个值为订阅者的方法,第二个值为订阅者的返回值

In [25]: result
Out[25]: [(<function __main__.receive_data(sender, **kw)>, 'received!')]

匿名信号

信号可以是匿名的,可以使用Signal类来创建唯一的信号(S大写,这个类不像之前的signal,为非单例模式)。
下面的on_readyon_complete为两个不同的信号

In [28]: from blinker import Signal

In [29]: class AltProcessor(object):
    ...:     on_ready = Signal()
    ...:     on_complete = Signal()
    ...:     
    ...:     def __init__(self, name):
    ...:         self.name = name
    ...:     
    ...:     def go(self):
    ...:         self.on_ready.send(self)
    ...:         print('Altername processing')
    ...:         self.on_complete.send(self)
    ...:         
    ...:     def __repr__(self):
    ...:         return '<AltProcessor {}>'.format(self.name)

通过装饰器来订阅

订阅者接收发布者传递的数据中简单地演示了使用装饰器来订阅,但是那种订阅方式不支持订阅指定的发布者,这时候我们可以用.connect_via(sender)

In [31]: @dice_roll.connect_via(1)
    ...: @dice_roll.connect_via(3)
    ...: @dice_roll.connect_via(5)
    ...: def odd_subscriver(sender):
    ...:     print('Observed dice roll {}'.format(sender))
    ...:     

In [32]: result = dice_roll.send(3)
Observed dice roll 3

In [33]: result = dice_roll.send(1)
Observed dice roll 1

In [34]: result = dice_roll.send(5)
Observed dice roll 5

In [35]: result = dice_roll.send(2)

检查信号是否有订阅者

In [37]: bool(signal('ready').receivers)
Out[37]: True

In [38]: bool(signal('complete').receivers)
Out[38]: False

In [39]: bool(AltProcessor.on_complete.receivers)
Out[39]: False

In [40]: signal('ready').has_receivers_for(processor_a)
Out[40]: True

参考

Blinker 官方文档

博客更新地址

Schemas是一种机器可读的文档,描述了API的端点,它们的URLs和所支持的操作。
Schemas可以用于自动生成文档,也可以用来驱动可以与API交互的动态客户端库。

Core API

为了提供Schemas支持,REST框架使用了Core API。
安装coreapi

pip install coreapi

添加schema

REST框架既支持自定义Schemas视图,也支持自动生成Schemas视图。
由于我们使用的是viewset和route,我们可以简单地自动生成Schemas视图。

现在我们需要通过在URL中配置一个自动生成的Schemas视图,为我们的API添加一个Schemas。
编辑urls.py

from rest_framework.schemas import get_schema_view

schema_view = get_schema_view(title='Pastebin API')

urlpatterns = [
    url(r'^schema/$', schema_view),
    ...
]

我们通过命令行的形式访问该接口,并指定接收格式为corejson

(django_rest_framework) [root@iZuf62kvdczytcyqlvr2pgZ django_rest_framework]# http https://127.0.0.1:80/schema/ Accept:application/coreapi+json
HTTP/1.0 200 OK
Allow: GET, HEAD, OPTIONS
Content-Length: 1498
Content-Type: application/coreapi+json
Date: Fri, 01 Dec 2017 12:04:53 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

{
    "_meta": {
        "title": "Pastebin API",
        "url": "https://127.0.0.1/schema/"
    },
    "_type": "document",
    "snippets": {
        "highlight": {
            "_type": "link",
            "action": "get",
...

使用命令行客户端

既然我们的API提供了一个Schemas url,我们可以使用一个动态的客户端来与API进行交互。
为了演示,我们使用Core API客户端。
安装coreapi-cli

pip install coreapi-cli

使用命令行客户端访问schema接口

(django_rest_framework) [root@iZuf62kvdczytcyqlvr2pgZ django_rest_framework]# coreapi get https://127.0.0.1:80/schema/
<Pastebin API "https://127.0.0.1/schema/">
    snippets: {
        list()
        read(id)
        highlight(id)
    }
    users: {
        list()
        read(id)
    }

由于我们没有登录,所以我们现在只能看到只读的接口。

列出现有的所有snippets

(django_rest_framework) [root@iZuf62kvdczytcyqlvr2pgZ django_rest_framework]# coreapi action snippets list
[
    {
        "url": "https://127.0.0.1/snippets/1/",
        "id": 1,
        "highlight": "https://127.0.0.1/snippets/1/highlight/",
        "owner": "song",
        "title": "test_1",
        "code": "print('hello world')",
        "linenos": true,
        "language": "python",
        "style": "friendly"
    },
]

有些API需要一些命名参数

(django_rest_framework) [root@iZuf62kvdczytcyqlvr2pgZ django_rest_framework]# coreapi action snippets highlight --param id=1
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "https://www.w3.org/TR/html4/strict.dtd">

<html>
<head>
  <title>test_1</title>
  <meta http-equiv="content-type" content="text/html; charset=None">
  <style type="text/css">
td.linenos { background-color: #f0f0f0; padding-right: 10px; }
span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }
pre { line-height: 125%; }
...

登录

需要写入的接口需要我们登录才能做,通过以下命令进行登录(test:qazxswedc为用户名和密码)

(django_rest_framework) [root@iZuf62kvdczytcyqlvr2pgZ django_rest_framework]# coreapi credentials add 127.0.0.1 test:qazxswedc --auth basic
Added credentials
127.0.0.1 "Basic dGVzdDpxYXp4c3dlZGM="

重载

(django_rest_framework) [root@iZuf62kvdczytcyqlvr2pgZ django_rest_framework]# coreapi reload
Pastebin API "https://127.0.0.1:80/schema/">
    snippets: {
        create(code, [title], [linenos], [language], [style])
        delete(id)
        highlight(id)
        list()
        partial_update(id, [title], [code], [linenos], [language], [style])
        read(id)
        update(id, code, [title], [linenos], [language], [style])
    }
    users: {
        list()
        read(id)
    }

我们现在可以对数据进行写操作了

coreapi action snippets delete --param id=1

关于

本人是初学Django REST framework,Django REST framework 学习纪要系列文章是我从官网文档学习后的初步消化成果,如有错误,欢迎指正。

学习用代码Github仓库:shelmingsong/django_rest_framework

本文参考的官网文档:Tutorial 7: Schemas & client libraries

博客更新地址

REST 框架包含了处理ViewSet的抽象,这样开发者就可以专注于API的状态和交互,而不用去管URL的构造,URL会按照
公共约定自动构造。
ViewSet类和View类差不多,不同的是ViewSet提供如read,update等方法,而不是get或是put
一个ViewSet类只绑定一组方法处理程序,当它被实例化为一组views时,通常用一个Router类来处理复杂的url。

重构代码以使用ViewSets

首先将我们现有的UserListUserDetail重构合并为UserViewSet
编辑views.py

from rest_framework import viewsets


class UserViewSet(viewsets.ReadOnlyModelViewSet):
    """
    这个ViewSet提供`list`和`detail`两个功能
    """
    queryset = User.objects.all()
    serializer_class = UserSerializer

这里我们使用的ReadOnlyModelViewSet类会提供默认的只读操作。
像以前一样,我们依然定义querysetserializer_class属性,只不过之前需要在两个类里面定义,现在只需要定义一遍。

接下来我们将现有的SnippetList, SnippetDetail, SnippetHighlight重构合并为SnippetViewSet
编辑views.py

from rest_framework.decorators import detail_route
from rest_framework.response import Response


class SnippetViewSet(viewsets.ModelViewSet):
    """
    这个ViewSet自动了`list`, `create`, `retrieve`, `update`和`destroy`功能
    我们需要另外定义一个`highlight`功能
    """
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly)

    @detail_route(renderer_classes=[renderers.StaticHTMLRenderer])
    def highlight(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

这里我们使用的ModelViewSet类会提供默认的读写操作。
注意,我们在此同样使用了@detail_route装饰器来创建一个额外的功能,名为highlight,当标准的create, update, delete不够用时,这个装饰器能够用来添加任何额外的功能。
使用@detail_route装饰器定制的额外功能会默认使用GET请求,我们可以在装饰器的参数内定义methods参数来使用如POST等等的其它请求。
默认情况下,这种定制的额外功能会使用和方法名同名的url,如想要使用与方法名不同的url,可以在detail_route装饰器中定义url_path参数。

将ViewSets与URLs绑定

编辑urls.py

from rest_framework import renderers
from snippets.views import api_root
from snippets.views import SnippetViewSet
from snippets.views import UserViewSet

snippet_list = SnippetViewSet.as_view({
    'get': 'list',
    'post': 'create'
})

snippet_detail = SnippetViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
})

snippet_highlight = SnippetViewSet.as_view(
    {'get': 'highlight'},
    renderer_classes=[renderers.StaticHTMLRenderer]
)

user_list = UserViewSet.as_view({
    'get': 'list'
})

user_detail = UserViewSet.as_view({
    'get': 'retrieve'
})

此时我们通过将http请求方式绑定到views的方法,从ViewSet类创建了一系列views。
接下来我们将这些views注册到url中
编辑urls.py

from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns

urlpatterns = format_suffix_patterns([
    url(r'^$', api_root),
    url(r'^snippets/$',
        snippet_list,
        name='snippet-list'),
    url(r'^snippets/(?P<pk>)[0-9]+/$',
        snippet_detail,
        name='snippet-detail'),
    url(r'^snippets/(?P<pk>[0-9]+)/highlight/$',
        snippet_highlight,
        name='snippet-highlight'),
    url(r'^users/$',
        user_list,
        name='user-list'),
    url(r'^users/(?P<pk>[0-9]+)/$',
        user_detail,
        name='user-detail')
])

使用Router

因为我们使用的是ViewSet类而不是View类,实际上我们不需要自己去设计URL。
通过使用Router类,我们只需要将合适的views注册到Router中,其它的事情就让它自动生成吧。

重写urls.py

from django.conf.urls import url
from django.conf.urls import include
from snippets import views
from rest_framework.routers import DefaultRouter


# 创建一个router并将viewsets注册上去
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet)
router.register(r'users', views.UserViewSet)

urlpatterns = [
    url(r'^', include(router.urls)),
    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

DefaultRouter类自动提供了根节点的url,所以我们不再需要单独去写。

关于

本人是初学Django REST framework,Django REST framework 学习纪要系列文章是我从官网文档学习后的初步消化成果,如有错误,欢迎指正。

学习用代码Github仓库:shelmingsong/django_rest_framework

本文参考的官网文档:Tutorial 6: ViewSets & Routers

博客更新地址

目前我们使用主键来表示模型之间的关系。在本章,我们将提高API的凝聚性和可读性。

为我们API的根节点创建URL

之前我们给snippetsusers创建了URL接口,但我们没有一个根节点的URL。
在此我们创建一个简单的,基于函数的views,并为它加上@api_view装饰器,修改snippets/views.py

from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse


@api_view(['GET'])
def api_root(request, format=None):
    return Response({
        'users': reverse('user-list', request=request, format=format),
        'snippets': reverse('snippet-list', request=request, format=format)
    })

需要注意两点:
* 我们使用REST框架中的reverse方法来返回完全符合规则的url
* url参数将会与我们之后在snippets/urls中定义的相同

编写snippets/urls.py

url(r'^$', views.api_root),

为高亮代码创建URL

接下来我们需要为高亮代码提供接口,和其它的接口不同,这个接口我们需要展现渲染好的HTML页面,而不是JSON。
REST框架给我们提供了两种HTML展现形式,一种是使用模板渲染,一种是使用已经提前渲染好的HTML,在此我们使用第二种。
另外,我们需要考虑当创建代码高亮的view时,我们没有现成的类来继承,因为我们不是返回对象,而是返回对象的一个属性,我们需要重写父类的get方法。
编辑snippets/views.py:

from rest_framework import renderers
from rest_framework.response import Response

class SnippetHighlight(generics.GenericAPIView):
    queryset = Snippet.objects.all()
    renderer_classes = (renderers.StaticHTMLRenderer,)

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)

编写snippets/urls.py

url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view()),

API之间使用超链接

在Web API中处理各个API之间的关系是一件非常头疼的事情,通常有以下方法表示关系:
* 使用主键
* 在API之间使用超链接
* 在关联API之间使用唯一字段表示
* 在关联API之间使用默认字符串表示
* 将一个API嵌套在另一个API类中
* 其它

REST框架支持上述所有方法,并且可以应用于正向、反向关系或类似外键这类自定义管理项。
在这里我们使用超链接来处理之间的关系,因此我们需要修改serializers,使用HyperlinkedModelSerializer替代原先的ModelSerializer:
* 默认不包含主键
* 使用HyperlinkedRelatedField时,需要在Meta子类的fields中包含“urls字段
* 使用
HyperlinkedRelatedField来代替PrimaryKeyRelatedField`表示关系

编辑snippets/serializers.py

class SnippetSerializer(serializers.HyperlinkedModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')

    class Meta:
        model = Snippet
        fields = ('url', 'id', 'highlight', 'owner',
                  'title', 'code', 'linenos', 'language', 'style')


class UserSerializer(serializers.HyperlinkedModelSerializer):
    snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)

    class Meta:
        model = User
        fields = ('url', 'id', 'username', 'snippets')

这里在SnippetSerializer中新增了一个highlight属性,这个属性和url字段类型相同,区别在于它指向的是snippet-highlight而非snippet-detail
另外,由于我们有.json格式的后缀,我们需要指定highlight字段使用.html来返回相应的格式

确保URL都被命名

在此之前我们创建了一些url参数,在此罗列:
* 根节点指向了user-listsnippet-list
* snippet serializer包含了一个指向snippet-highlight的url的字段
* user serializer包含了指向snippet-detail的url的字段
* snippet serializeruser serializer都包含了url字段,这个字段默认指向{model_name}-detail,这里分别是snippet-detailuser-detail

编辑snippet/urls.py

from django.conf.urls import url, include
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

# API endpoints
urlpatterns = format_suffix_patterns([
    url(r'^$', views.api_root),
    url(r'^snippets/$',
        views.SnippetList.as_view(),
        name='snippet-list'),
    url(r'^snippets/(?P<pk>[0-9]+)/$',
        views.SnippetDetail.as_view(),
        name='snippet-detail'),
    url(r'^snippets/(?P<pk>[0-9]+)/highlight/$',
        views.SnippetHighlight.as_view(),
        name='snippet-highlight'),
    url(r'^users/$',
        views.UserList.as_view(),
        name='user-list'),
    url(r'^users/(?P<pk>[0-9]+)/$',
        views.UserDetail.as_view(),
        name='user-detail')
])

# Login and logout views for the browsable API
urlpatterns += [
    url(r'^api-auth/', include('rest_framework.urls',
                               namespace='rest_framework')),
]

添加分页

后面可能会有很多个数据产生,我们需要对返回结果进行分页,修改tutorial/settings.py

REST_FRAMEWORK = {
    'PAGE_SIZE': 10
}

注意,所有关于REST框架的settings都在一个叫做REST_FRAMEWORK的字典中,这帮助我们与其他的settings分离开来。

关于

本人是初学Django REST framework,Django REST framework 学习纪要系列文章是我从官网文档学习后的初步消化成果,如有错误,欢迎指正。

学习用代码Github仓库:shelmingsong/django_rest_framework

本文参考的官网文档:Tutorial 5: Relationships & Hyperlinked APIs

博客更新地址

目前为止,我们的代码没有限制谁可以编辑和删除代码片段,此节我们需要实现以下功能
* 代码片段需要与创建者关联
* 只有通过验证的用户才能创建代码片段
* 只有创建者才能修改或删除代码片段
* 没有通过验证的用户拥有只读权限

给model添加字段

我们需要添加两个字段,一个用于存储代码片段的创建者信息,一个用于存储代码的高亮信息

    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
    owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
    highlighted = models.TextField()

同时,我们需要在该模型类执行保存操作时,自动填充highlighted字段,使用pygments库。
首先,导入一些包

from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

然后为Snippet重写父类的save方法

    def save(self, *args, **kwargs):
        """
        use the 'pygments' library to create a highlighted HTML
        representation of code snippet
        """
        lexer = get_lexer_by_name(self.language)
        linenos = self.linenos and 'table' or False
        options = self.title and {'title': self.title} or {}
        formatter = HtmlFormatter(style=self.style, linenos=linenos,
                                  full=True, **options)
        self.highlighted = highlight(self.code, lexer, formatter)
        super(Snippet, self).save(*args, **kwargs)

接下来需要迁移数据库,方便起见,删库,然后重新迁移

(django_rest_framework) [root@localhost tutorial]# rm -f tmp.db db.sqlite3 && \
> rm -rf snippets/migrations/ && \
> python manage.py makemigrations snippets && \
> python manage.py migrate
Migrations for 'snippets':
  snippets/migrations/0001_initial.py
    - Create model Snippet
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions, snippets
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying sessions.0001_initial... OK
  Applying snippets.0001_initial... OK

为了测试API,我们需要创建一些用户,最快的方式就是通过createsuperuser命令

(django_rest_framework) [root@localhost tutorial]# python manage.py createsuperuser
Username (leave blank to use 'root'): song
Email address: shelmingsong@gmail.com
Password: 
Password (again): 
Superuser created successfully.
(django_rest_framework) [root@localhost tutorial]# python manage.py createsuperuser
Username (leave blank to use 'root'): user_1
Email address: user_1@gmail.com
Password: 
Password (again): 
Superuser created successfully.
(django_rest_framework) [root@localhost tutorial]# python manage.py createsuperuser
Username (leave blank to use 'root'): user_2
Email address: user_2@gmail.com
Password: 
Password (again): 
Superuser created successfully.

为用户模型添加接口

我们已经创建了三个用户,现在我们需要添加用户相关的接口,修改serializers.py

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ('id', 'username', 'snippets')

因为snippetsuser是一种反向的关联,默认不会包含入ModelSerializer类中,所以需要我们手动添加

我们也需要对views.py进行修改,由于用户页面为只读,所以继承于ListAPIViewRetrieveAPIView

from django.contrib.auth.models import User
from snippets.serializers import UserSerializer


class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

配置url.py

    url(r'^users/$', views.UserList.as_view()),
    url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),

关联User和Snippet

此时我们创建一个代码片段,是无法与用户关联的,因为用户信息是通过request获取的。
因此我们需要重写snippet的view中perform_create()方法,这个方法允许我们在对象保存前进行相关操作,处理任何有requestrequested URL传递进来的数据

修改views.py中的SnippetList类,添加perform_create()方法

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

如此,新建代码片段时,会添加owner字段,该字段存储了request中的用户信息

更新serializer

之前我们在views中的SnippetList类中添加了perform_create方法,保存了owner信息,因而也需要在serializer中的SnippetSerializer类中添加owner信息,同时将owner添加进Meta子类的fields字段中

class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'linenos', 'language', 'style', 'owner')

这里我们使用了ReadOnlyField类型,这个类型是只读的,不能被更新,和Charfield(read_only=True)是一样的效果

添加权限认证

我们希望只有登录的用户能够去增加代码片段,未登录则只有查看的权限,此时我们需要用到IsAuthenticatedOrReadOnly

修改views.py,为snippet的两个类views添加permission_classes字段

from rest_framework import permissions


class SnippetList(generics.ListCreateAPIView):
    permission_classes = (permissions.IsAuthenticatedOrReadOnly, )


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = (permissions.IsAuthenticatedOrReadOnly, )

添加登陆接口

修改项目的urls.py

urlpatterns = [
    url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]

r'^api-auth/'可以自定,namespace在Django 1.9 + 的版本中可以省略

运行Django服务器,访问your.domain/snippets/,点击右上角的登陆按钮,登陆我们之前创建的用户后,就可以创建代码片段了

创建完几个代码片段后,再访问your.domain/users/时,就可以看到每个用户创建了哪几个代码片段了

对象级别的权限

现在用户都可以对所有的snippets进行增删改查,我们要确保只有创建者可以对snippets进行改动或删除。

snippetsapp中,创建permissions.py

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    custom permission to only allow owners of an object to edit it
    """
    def has_object_permission(self, request, view, obj):
        # allow all user to read
        if request.method in permissions.SAFE_METHODS:
            return True

        # only allow owner to edit
        return obj.owner == request.user

views.py中添加权限

from snippets.permissions import IsOwnerOrReadOnly

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly)

此时我们访问your.domain/snippets/1/,若用户未登录或登录用户不是该snippets的创建者,则只有读的权限,页面上表现为没有DELETE(上方中间)和PUT(右下角)按钮

通过接口进行权限认证

之前我们是通过浏览器页面进行登录的,而当我们直接使用接口去请求时,如果没有进行登录,而对某个snippet进行修改或是创建一个新的snippet,则会报错

(django_rest_framework) [root@localhost django_rest_framework]# http POST https://127.0.0.1:80/snippets/ code="hahah"
HTTP/1.0 403 Forbidden
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 58
Content-Type: application/json
Date: Tue, 28 Nov 2017 14:56:18 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

{
    "detail": "Authentication credentials were not provided."
}

(django_rest_framework) [root@localhost django_rest_framework]# http POST https://127.0.0.1:80/snippets/1/ code="hahah"
HTTP/1.0 403 Forbidden
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Length: 58
Content-Type: application/json
Date: Tue, 28 Nov 2017 14:56:26 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

{
    "detail": "Authentication credentials were not provided."
}

我们在发送请求时,提供用户名和密码,就可以进行操作了

(django_rest_framework) [root@localhost django_rest_framework]# http -a your_username:your_password POST https://127.0.0.1:80/snippets/ code="hahah"
HTTP/1.0 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 104
Content-Type: application/json
Date: Tue, 28 Nov 2017 14:58:10 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

{
    "code": "hahah",
    "id": 4,
    "language": "python",
    "linenos": false,
    "owner": "song",
    "style": "friendly",
    "title": ""
}

关于

本人是初学Django REST framework,Django REST framework 学习纪要系列文章是我从官网文档学习后的初步消化成果,如有错误,欢迎指正。

学习用代码Github仓库:shelmingsong/django_rest_framework

本文参考的官网文档:Tutorial 4: Authentication & Permissions

博客更新地址

基于类的views

之前我们创建的views都是基于函数的,我们也可以基于类来写views

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status


class SnippetList(APIView):
    """
    list all snippets, or create a new snippet
    """
    def get(self, request, format=None):
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = SnippetSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class SnippetDetail(APIView):
    """
    retrieve, update or delete a snippet instance
    """
    def get_object(self, pk):
        try:
            return Snippet.objects.get(pk=pk)
        except Snippet.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        snippet = self.get_object(pk)
        serializer = SnippetSerializer(snippet)
        return Response(serializer.data)

    def put(self, request, pk, format=None):
        snippet = self.get_object(pk)
        serializer = SnippetSerializer(snippet, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, format=None):
        snippet = self.get_object()
        snippet.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

接下来相应地去修改urls.py

from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    url(r'^snippets/$', views.SnippetList.as_view()),
    url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

使用mixins

相比函数型views来说,基于类的views可以很容易地通过继承来去复用代码。
刚才我们创建的API中,基本都是数据库的增删改查操作,这种常见的操作都已经在REST框架中定义好了,在mixin类中,我们只需要去继承它。
再次重构views.py

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics


class SnippetList(mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  generics.GenericAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

SnippetList这个类,首先继承于GenericAPIView,这个类提供了核心功能,接下来是继承于ListModelMixinCreateModelMixin类,这两个类分别提供了.list().create()功能。

接下来重构下一个API

class SnippetDetail(mixins.RetrieveModelMixin,
                    mixins.UpdateModelMixin,
                    mixins.DestroyModelMixin,
                    generics.GenericAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin分别提供了retrieve(), update(), destroy()功能

继承更通用的generic类

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import generics


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

关于

本人是初学Django REST framework,Django REST framework 学习纪要系列文章是我从官网文档学习后的初步消化成果,如有错误,欢迎指正。

学习用代码Github仓库:shelmingsong/django_rest_framework

本文参考的官网文档:Tutorial 3: Class-based Views

博客更新地址

Request 对象

REST 框架引入了Request对象,继承于HttpRequest,相比HttpRequest提供了更多请求解析,最核心的功能是request.data属性,类似于request.POST,以下是不同之处。
* request.POST
1. 只能处理form表单数据;
2. 只能处理POST请求。
* request.data
1. 能够处理任意一种数据;
2. 能够处理POST、PUT、PATCH请求

Response对象

REST框架也引入了Response对象,它是一个TemplateResponse类型,能够将未处理的文本转换为合适的类型返回给客户端

return Response(data)

状态码

REST框架提供了更可读的状态信息,比如HTTP_400_BAD_REQUEST

API views封装

  • 对于函数views,可以使用@api_view装饰器
  • 对于类views,可以继承于APIView

views应用

  • 修改snippets/views.py
  1. GET获取所有code snippets,与新建code snippet的接口
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer


@api_view(['GET', 'POST'])
def snippet_list(request):
    """
    list all code snippets, or create a new snippet
    """
    if request.method == 'GET':
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    elif request.method == 'POST':
        serializer = SnippetSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
  1. GET获取单个code snippetPUT更新单个code snippetDELETE删除单个code snippet
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
    """
    retrieve, update or delete code snippet
    """
    try:
        snippet = Snippet.objects.get(pk=pk)
    except Snippet.DoseNotExist:
        return Response(status=status.HTTP_404_NOT_FOUND)

    if request.method == 'GET':
        serializer = SnippetSerializer(snippet)
        return Response(serializer.data)

    elif request.method == 'PUT':
        serializer = SnippetSerializer(snippet, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    elif request.method == 'DELETE':
        snippet.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

和上一步的views明显不同的是,我们不再需要关心输入(request)输出(response)的数据类型,REST框架已经帮我们处理好了

为URLs添加可选格式后缀

如上一步所说的,REST框架已经帮我们处理好了输入(request)输出(response)的数据类型,也就意味着一个API可以去处理不同的数据类型,在URLs中使用格式后缀可以帮助我们处理类似这样的url: https://192.168.0.103/snippets.json

  • 首先我们需要在views中添加形参format=None
def snippet_list(request, format=None):

def snippet_list(request, format=None):
  • 然后我们修改urls.py
from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    url(r'^snippets/$', views.snippet_list),
    url(r'^snippets/(?P<pk>[0-9]+)$', views.snippet_detail),
]

urlpatterns = format_suffix_patterns(urlpatterns)

调用接口

  • 在启动服务器前,先修改settings.py中的ALLOWED_HOSTS,方便后面通过外部浏览器请求接口
ALLOWED_HOSTS = ['*']
  • 启动服务器(别管时间,每天晚上回来,让Win10从睡眠状态恢复,虚拟机的IP和时区总会变 🙁 ,懒得每次都改了)
(django_rest_framework) [root@localhost tutorial]# python manage.py runserver 0:80
Performing system checks...

System check identified no issues (0 silenced).
November 21, 2017 - 02:47:02
Django version 1.11.7, using settings 'tutorial.settings'
Starting development server at https://0:80/
Quit the server with CONTROL-C.
  • 打开另一个shell窗口,发送请求
(django_rest_framework) [root@localhost django_rest_framework]# http https://127.0.0.1:80/snippets/
HTTP/1.0 200 OK
Allow: POST, GET, OPTIONS
Content-Length: 505
Content-Type: application/json
Date: Mon, 20 Nov 2017 18:51:07 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

[
    {
        "code": "foo = \"bar\n\"",
        "id": 1,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
    {
        "code": "print \"hello, world\"\n",
        "id": 2,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
...
]
  • 我们可以添加HTTP HEADERS来控制返回数据的数据类型
  1. json
(django_rest_framework) [root@localhost django_rest_framework]# http https://127.0.0.1:80/snippets/ Accept:application/json
HTTP/1.0 200 OK
Allow: POST, GET, OPTIONS
Content-Length: 505
Content-Type: application/json
Date: Mon, 20 Nov 2017 18:52:27 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

[
    {
        "code": "foo = \"bar\n\"",
        "id": 1,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
    {
        "code": "print \"hello, world\"\n",
        "id": 2,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
]
  1. html
(django_rest_framework) [root@localhost django_rest_framework]# http https://127.0.0.1:80/snippets/ Accept:text/html
HTTP/1.0 200 OK
Allow: POST, GET, OPTIONS
Content-Length: 8139
Content-Type: text/html; charset=utf-8
Date: Mon, 20 Nov 2017 18:53:39 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

<!DOCTYPE html>
<html>
  <head>



        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="robots" content="NONE,NOARCHIVE" />


      <title>Snippet List – Django REST framework</title>



          <link rel="stylesheet" type="text/css" href="/static/rest_framework/css/bootstrap.min.css"/>
...
  • 或者我们直接可以添加url后缀来控制返回数据的数据类型
  1. json
(django_rest_framework) [root@localhost django_rest_framework]# http https://127.0.0.1:80/snippets.json
HTTP/1.0 200 OK
Allow: POST, GET, OPTIONS
Content-Length: 505
Content-Type: application/json
Date: Mon, 20 Nov 2017 18:55:27 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

[
    {
        "code": "foo = \"bar\n\"",
        "id": 1,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
    {
        "code": "print \"hello, world\"\n",
        "id": 2,
        "language": "python",
        "linenos": false,
        "style": "friendly",
        "title": ""
    },
...
]
  1. html
(django_rest_framework) [root@localhost django_rest_framework]# http https://127.0.0.1:80/snippets.api
HTTP/1.0 200 OK
Allow: POST, GET, OPTIONS
Content-Length: 8160
Content-Type: text/html; charset=utf-8
Date: Mon, 20 Nov 2017 18:56:35 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

<!DOCTYPE html>
<html>
  <head>



        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="robots" content="NONE,NOARCHIVE" />


      <title>Snippet List – Django REST framework</title>



          <link rel="stylesheet" type="text/css" href="/static/rest_framework/css/bootstrap.min.css"/>
          <link rel="stylesheet" type="text/css" href="/static/rest_framework/css/bootstrap-tweaks.css"/>
...
  • 类似的,我们可以发送不同类型的数据给API
  1. post form data
(django_rest_framework) [root@localhost django_rest_framework]# http --form POST https://127.0.0.1:80/snippets/ code="hello world post form data"
HTTP/1.0 201 Created
Allow: POST, GET, OPTIONS
Content-Length: 110
Content-Type: application/json
Date: Mon, 20 Nov 2017 18:58:58 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

{
    "code": "hello world post form data",
    "id": 6,
    "language": "python",
    "linenos": false,
    "style": "friendly",
    "title": ""
}
  1. post json data
(django_rest_framework) [root@localhost django_rest_framework]# http --json POST https://127.0.0.1:80/snippets/ code="hello world post json data"
HTTP/1.0 201 Created
Allow: POST, GET, OPTIONS
Content-Length: 110
Content-Type: application/json
Date: Mon, 20 Nov 2017 18:59:44 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

{
    "code": "hello world post json data",
    "id": 7,
    "language": "python",
    "linenos": false,
    "style": "friendly",
    "title": ""
}
  • 在请求时添加--debug后缀可以查看请求的详细信息
(django_rest_framework) [root@localhost django_rest_framework]# http --json POST https://127.0.0.1:80/snippets/ code="hello world post json data" --debug
HTTPie 0.9.9
Requests 2.18.4
Pygments 2.2.0
Python 3.6.3 (default, Nov  4 2017, 22:19:41) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-16)]
/root/.pyenv/versions/3.6.3/envs/django_rest_framework/bin/python
Linux 3.10.0-693.el7.x86_64

<Environment {
    "colors": 8,
    "config": {
        "__meta__": {
            "about": "HTTPie configuration file",
            "help": "https://httpie.org/docs#config",
            "httpie": "0.9.9"
        },
        "default_options": "[]"
    },
    "config_dir": "/root/.httpie",
    "is_windows": false,
    "stderr": "<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>",
    "stderr_isatty": true,
    "stdin": "<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>",
    "stdin_encoding": "UTF-8",
    "stdin_isatty": true,
    "stdout": "<_io.TextIOWrapper name='<stdout>' mode='w' encoding='UTF-8'>",
    "stdout_encoding": "UTF-8",
    "stdout_isatty": true
}>

>>> requests.request(**{
    "allow_redirects": false,
    "auth": "None",
    "cert": "None",
    "data": "{\"code\": \"hello world post json data\"}",
    "files": {},
    "headers": {
        "Accept": "application/json, */*",
        "Content-Type": "application/json",
        "User-Agent": "HTTPie/0.9.9"
    },
    "method": "post",
    "params": {},
    "proxies": {},
    "stream": true,
    "timeout": 30,
    "url": "https://127.0.0.1:80/snippets/",
    "verify": true
})

HTTP/1.0 201 Created
Allow: POST, GET, OPTIONS
Content-Length: 110
Content-Type: application/json
Date: Mon, 20 Nov 2017 19:00:45 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Vary: Accept, Cookie
X-Frame-Options: SAMEORIGIN

{
    "code": "hello world post json data",
    "id": 9,
    "language": "python",
    "linenos": false,
    "style": "friendly",
    "title": ""
}
  • 在浏览器中发送请求
  1. 在浏览器中发送请求,默认会返回html类型的数据

  2. 可以像之前那样,加上url后缀,来请求json数据

关于

本人是初学Django REST framework,Django REST framework 学习纪要系列文章是我从官网文档学习后的初步消化成果,如有错误,欢迎指正。

学习用代码Github仓库:shelmingsong/django_rest_framework

本文参考的官网文档:Tutorial 2: Requests and Responses

博客更新地址