前言
繼上一篇文章 https://jerryeml.coderbridge.io/2021/05/30/create-api-with-pyhon-and-test-with-auto-retry/
感謝同事 Robin
的建議 推薦 Flask
這個更輕量的東西讓我去嘗試實作 API
的確是更方便一些 因為真的幫忙包了好多東西XD
上一篇提到 透過 #BaseHTTPRequestHandler
可以 mock api
的 response code
但萬一今天想驗證的除了 response code
之外 還包含回傳的 json
呢
有沒有更好的方法可以快速把這些東西 implement
出來
答案是有的!! 接著就可以用 pytest
針對他做 testing
囉
Flask
Flask
是一個使用 Python
編寫的輕量級 Web
應用框架。基於 Werkzeug WSGI
工具箱和 Jinja2
模板引擎。 Flask
使用 BSD
授權。
Flask
被稱為「微框架」,因為它使用簡單的核心,用擴充增加其他功能。 Flask
沒有預設使用的資料庫、表單驗證工具。然而, Flask
保留了擴增的彈性,可以用 Flask-extension
加入這些功能: ORM
、表單驗證工具、檔案上傳、各種開放式身分驗證技術。
但我們今天不會介紹 Flask-extension
我們的目的只是介紹如何利用 flask
快速建置一個可被驗證的 API
關於 API
API(application programming interface)是應用程式的介面/接口,我們不需要知道背後怎麼執行,只要瞭解要跟它說什麼、以及我們可以拿回什麼。
就像我們去速食店櫃檯點餐,櫃檯店員就像API接口,替櫃檯前的顧客、和後台廚房建立起友誼的橋樑。
當我們和他說「請給我雞腿堡」,不需要管什麼原料、漢堡如何製作,稍待片刻廚房就會完成,並透過櫃檯店員轉手拿到我們面前的餐盤。
我們在接口這端(櫃台店員)說需求,接著他會進行處理,例如去資料庫(廚房)拿東西,處理後告訴我們結果;過程我們只需要等待即可。
REST API(Representational State Transfer API)是一種API的設計風格,關於我們怎麼和資料互動;這邊不深究細節,讀者可以想成是某個共識,例如請、謝謝、對不起,出現的語境會有對應的行為。
前置準備
- 環境設定的部分可以參考給Python一個虛擬的家
pip install flask
pip install requests
30秒搞一個API
產生一個 py
檔案 >> api_with_flask.py
import flask
from flask import jsonify, request
from flask import render_template
app = flask.Flask(__name__)
app.config["DEBUG"] = True
@app.route('/', methods=['GET'])
def home():
return "<h1>Hello, I am here !</h1>"
@app.route('/template', methods=['GET'])
def template_home():
return render_template('upload_sample.txt')
app.run()
接著輸入 python api_with_flask.py
就可以發現你的local api已經建立好囉
依照畫面提示打開瀏覽器前往 http://127.0.0.1:5000/
接著來實作各種Action吧
在此之前 必須先定義好 API
裡面的 DATASET
我們就假設有一個復仇者聯盟裡面有以下成員吧XD
avergers:[aladdin, elpis, lapras]
,
其下也包含各自的屬性及內容,id
、Leader
、national
、superpower
、nickname
、gender
等
# test data
aladdin = {
"id":1,
"Leader":"Tony",
"nickname":"iron man",
"nationality":"American",
"gender":"M",
"superpower":"n"
}
elpis = {
"id":2,
"Leader":"Peter",
"nickname":"spider man",
"nationality":"American",
"gender":"M",
"superpower":"y"
}
lapras = {
"id":3,
"Leader":"Natasha",
"nickname":"Block Widow",
"nationality":"Russia",
"gender":"F",
"superpower":"n"
}
avengers =[aladdin, elpis, lapras]
當我們把 DATASET
加入程式碼之後
就可以來進行實作囉
GET
GET
方法請求展示指定資源。使用 GET
的請求只應用於取得資料。
首先我們一樣
@app.route('/get/avengers/all', methods=['GET'])
def avengers_all():
return jsonify(avengers)
jsonify 是甚麼呢
藉由 Flask
套件所提供的 jsonify()
函數輸出為 JSON
格式呈現在網頁上
那如果沒有用 jsonify
會怎麼樣?
TypeError
TypeError: The view function did not return a valid response. The return type must be a string, dict, tuple, Response instance, or WSGI callable, but it was a list.
沒錯…會壞掉。
一切正常之後 輸入 http://127.0.0.1:5000/get/avengers/all
就可以得到 API
的內容囉
我們還可以多寫一個 API
讓大家帶參數來 Query
想要的東西
舉個例子來說 我們開一個 http://127.0.0.1:5000/get/avengers
的 API
可以指定參數拿特定的資料回來
@app.route('/get/avengers', methods=['GET'])
def avengers_properties():
results = []
nationality = ""
if 'nationality' in request.args:
nationality = request.args['nationality']
else:
print("no hero")
for avenger in avengers:
if avenger['nationality'] == nationality:
results.append(avenger)
return jsonify(results)
假設我們輸入後 URL
帶入 ?nationality=American
會拿到 nationality=American
的資料囉
[
{
"Leader": "Tony",
"gender": "M",
"id": 1,
"nationality": "American",
"nickname": "iron man",
"superpower": "n"
},
{
"Leader": "Peter",
"gender": "M",
"id": 2,
"nationality": "American",
"nickname": "spider man",
"superpower": "y"
}
]
POST
POST
方法用於提交指定資源的實體,通常會改變伺服器的狀態或副作用(side effect)
。
POST
方法新增一位avenger
到avengers
,並回傳新增avenger
的JSON
。- 將我們
POST
送出的JSON
資料,轉換成Python
的dictionary
,後續建立new_avenger
的dictionary
,並把它加入avengers
清單。 - 回傳剛剛建立的
new_avengers json
格式,讓使用者知道新增成功。
@app.route('/post/avengers', methods=['POST'])
def create_avengers():
request_data = request.get_json()
leader = request_data['Leader']
gender = request_data['gender']
new_avenger = {
"Leader": leader,
"gender": gender,
}
avengers.append(new_avenger)
return jsonify(new_avenger)
利用 POSTMAN
嘗試 就可以發現 成功囉
小結
從上述兩個例子,可以看到 Python
處理資料後、會透過 jsonify
把 dictionary
轉成 JSON
格式再回傳,顯示在瀏覽器、 Postman
。
而 Postman
透過 Post
方法傳給 Python
的 JSON
,會先被轉成 Python
的 dictionary
格式進行處理,而後透過 jsonify
把 dictionary
轉成 JSON
再回傳。
接著我們來測試 API 吧!! with pytest
建立一個 test_api_with_flask.py
我們先利用 pytest
的 fixture
來做 setup
的動作
目的是 init
我們剛寫好的 api
並且把我們自己建立的 api
利用另外一條 thread
給 create
出來
import logging
import json
import requests
import pytest
from threading import Thread
from tmp import api_with_flask
@pytest.fixture(scope="module", autouse=True)
def setup():
# Start running mock server in a separate thread.
# Daemon threads automatically shut down when the main process exits.
mock_server_thread = Thread(target=api_with_flask.init_api)
mock_server_thread.setDaemon(True)
mock_server_thread.start()
接著呢 我們開始寫第一個 testcase
主要會去驗證這個 api
的 return code
是不是 200
還有內容是不是我們預期的
def test_avengers_all_with_get_method():
rtn = requests.get(url='http://localhost:5000/get/avengers/all')
print(f'status code: {rtn.status_code}')
assert rtn.status_code == 200
print(f'response: {json.loads(rtn.content)}')
expect_content = [
{
"Leader": "Tony",
"gender": "M",
"id": 1,
"nationality": "American",
"nickname": "iron man",
"superpower": "n"
},
{
"Leader": "Peter",
"gender": "M",
"id": 2,
"nationality": "American",
"nickname": "spider man",
"superpower": "y"
},
{
"Leader": "Natasha",
"gender": "F",
"id": 3,
"nationality": "Russia",
"nickname": "Block Widow",
"superpower": "n"
}
]
assert json.loads(rtn.content) == expect_content
輸入 pytest -s .\tmp\test_api_with_flask.py
透過 -s
可以知道 印出來的log是甚麼
接著我們可以繼續下一個 testcsae
透過這個反例 我們打算測試 api
有沒有開放 post
的功能
預期是沒有 所以應該要回傳 405
def test_avengers_all_with_post_method():
rtn = requests.post(url='http://localhost:5000/get/avengers/all')
print(f'status code: {rtn.status_code}')
assert rtn.status_code == 405
測試結果
成功的結果如下
================================== test session starts ===================================
platform win32 -- Python 3.8.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: E:\Coding\side_project\poc\Python_Practice
collected 2 items
tmp\test_api_with_flask.py * Serving Flask app 'tmp.api_with_flask' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
status code: 200
response: [{'Leader': 'Tony', 'gender': 'M', 'id': 1, 'nationality': 'American', 'nickname': 'iron man', 'superpower': 'n'}, {'Leader': 'Peter', 'gender': 'M', 'id': 2, 'nationality': 'American', 'nickname': 'spider man', 'superpower': 'y'}, {'Leader': 'Natasha', 'gender': 'F', 'id': 3, 'nationality': 'Russia', 'nickname': 'Block Widow', 'superpower': 'n'}]
.status code: 405
.
=================================== 2 passed in 4.54s ====================================
完整程式碼
api_with_flask.py
import flask
from flask import jsonify, request
from flask import render_template
app = flask.Flask(__name__)
app.config["DEBUG"] = True
# test data
aladdin = {
"id":1,
"Leader":"Tony",
"nickname":"iron man",
"nationality":"American",
"gender":"M",
"superpower":"n"
}
elpis = {
"id":2,
"Leader":"Peter",
"nickname":"spider man",
"nationality":"American",
"gender":"M",
"superpower":"y"
}
lapras = {
"id":3,
"Leader":"Natasha",
"nickname":"Block Widow",
"nationality":"Russia",
"gender":"F",
"superpower":"n"
}
avengers =[aladdin, elpis, lapras]
@app.route('/', methods=['GET'])
def home():
return "<h1>Hello, I am here !</h1>"
@app.route('/template', methods=['GET'])
def template_home():
return render_template('upload_sample.txt')
@app.route('/get/avengers/all', methods=['GET'])
def avengers_all():
return jsonify(avengers)
@app.route('/get/avengers', methods=['GET'])
def avengers_properties():
results = []
nationality = ""
if 'nationality' in request.args:
nationality = request.args['nationality']
else:
print("no hero")
for avenger in avengers:
if avenger['nationality'] == nationality:
results.append(avenger)
return jsonify(results)
@app.route('/post/avengers', methods=['POST'])
def create_avengers():
request_data = request.get_json()
leader = request_data['Leader']
gender = request_data['gender']
new_avenger = {
"Leader": leader,
"gender": gender,
}
avengers.append(new_avenger)
return jsonify(new_avenger)
def init_api():
app.run(debug=False, use_reloader=False)
if __name__ == '__main__':
init_api()
api_with_flask.py
import logging
import json
import requests
import pytest
from threading import Thread
from tmp import api_with_flask
@pytest.fixture(scope="module", autouse=True)
def setup():
# Start running mock server in a separate thread.
# Daemon threads automatically shut down when the main process exits.
mock_server_thread = Thread(target=api_with_flask.init_api)
mock_server_thread.setDaemon(True)
mock_server_thread.start()
def test_avengers_all_with_get_method():
rtn = requests.get(url='http://localhost:5000/get/avengers/all')
print(f'status code: {rtn.status_code}')
assert rtn.status_code == 200
print(f'response: {json.loads(rtn.content)}')
expect_content = [
{
"Leader": "Tony",
"gender": "M",
"id": 1,
"nationality": "American",
"nickname": "iron man",
"superpower": "n"
},
{
"Leader": "Peter",
"gender": "M",
"id": 2,
"nationality": "American",
"nickname": "spider man",
"superpower": "y"
},
{
"Leader": "Natasha",
"gender": "F",
"id": 3,
"nationality": "Russia",
"nickname": "Block Widow",
"superpower": "n"
}
]
assert json.loads(rtn.content) == expect_content
def test_avengers_all_with_post_method():
rtn = requests.post(url='http://localhost:5000/get/avengers/all')
print(f'status code: {rtn.status_code}')
assert rtn.status_code == 405
https://github.com/jerryeml/programming.py
Reference
https://www.tpisoftware.com/tpu/articleDetails/1719
https://medium.com/datainpoint/flask-web-api-quickstart-3b13d96cccc2