2023年8月9日 星期三

Flask網頁開發 - 第二版 - 本書使用的輔助套件與工具 (1)

  選擇閱讀Miguel Grinberg著作,賴屹民翻譯的《Flask網頁開發 第二版》(Flask Web Development, 2nd Edition)這本書,是縱觀我目前所擁有的資訊技能當中,最薄弱的一塊,若想要長久地發展Data ScienceDevOps,甚至DataOps,網頁開發肯定也是無法迴避技能之一,有鑑於自己最熟悉的程式語言仍舊是Python,加上目前工作潛在的網頁開發需求,多為小型的Demo與試驗,因此選擇學習輕量級框架Flask,而非選擇學習重量級框架Django

  本書的介紹方式是以部落格網站為設計範例,以新增網站功能的方式,逐一介紹如何以Flask及其相關套件做出這樣的功能,本書作者所使用的工具相當繁雜,若對這些工具沒有一定程度的熟悉度,閱讀本書的過程會和我一樣感到「舉步維艱」,每閱讀一頁新的內容,都要再花額外的心力補充應該具備的先備知識,長期下來的結果便是讀書時程拉長,且失去網頁開發架構的整體性與邏輯性,因此我打算先將不熟悉的先備知識整理為一篇網誌,再來重新學習Flask架構,目標不只是要學好Flask,更要能隨心所欲地使用它。

一、使用faker套件建立偽造的部落格文章資料用以測試

faker套件

l   安裝faker套件:
pip install faker

l   faker套件文件:
https://faker.readthedocs.io/en/master/

l   faker套件基本用法:
import faker

faker.Faker().email()
à 'lopezscott@example.org'
faker.Faker().user_name()
à 'ramseyeric'
faker.Faker().name()
à 'Andrea Howard'
faker.Faker().city()
à 'East Chadbury'
faker.Faker().text()
à 'His against gas will TV.\nApply together add individual something station dinner. Chair bar research account specific car compare. Free during because less from there.\nLose room him heavy step.'
faker.Faker().past_date()
à datetime.date(2023, 6, 27)

二、使用API相關套件

補充FastAPI

l   資料來源與先備知識:
FastAPI 入門筆記
什麼是 WSGI & ASGI ?
WSGI & ASGI

l   安裝FastAPI
pip install fastapi

l   FastAPI套件文件:
https://fastapi.tiangolo.com/

l   安裝ASGI Server
pip install uvicorn

l   GET方法範例:
# main.py
from typing import Optional
from fastapi import FastAPI

app = FastAPI()

# Asynchronize Function
非同步處理在閒置時,可以將注意力轉移到其它事情上
@app.get("/")
async def read_root():
    return {"Hello": "Kitty"}

# FastAPI
在建立API時,需要以型態提示(Typing Hints)來定義參數的型態
@app.get("/users/{user_id}")
async def read_user(user_id: int, q1: str="timmy", q2: Optional[str] = None):
    return {"user_id": user_id, "q1": q1, "q2": q2}

l   Terminal啟動服務:
# uvicorn <
檔名>:<app名稱>
# --reload
在專案更新時,自動重新載入,類似Flaskdebug模式
uvicorn main:app --reload

l   開啟瀏覽器測試API
http://localhost:8000/
{"Hello":"Kitty"}

http://localhost:8000/users/21
{"user_id":21,"q1":"timmy","q2":null}

http://localhost:8000/users/21?q1=tingyu&q2=nieh
{"user_id":21,"q1":"tingyu","q2":"nieh"}

l   FastAPI產生的說明文件:
http://localhost:8000/docs
http://localhost:8000/redoc

l   裝飾器定義常見的HTTP Request方法:

GET

@app.get(路徑)

取得資料

POST

@app.post(路徑)

建立資料

PUT

@app.put(路徑)

更新資料

DELETE

@app.delete(路徑)

刪除資料

l   PUT方法範例:
# main.py
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float
    is_offer: Optional[bool] = None

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_name": item.name, "item_id": item_id}

HTTPie套件

l   HTTPie測試Web服務,在命令列測試Python Web服務最常用的兩種用戶端是cURLHTTPie,而HTTPie是專為API請求量身訂做的:
pip install httpie

l   HTTPie套件文件:
https://httpie.io/docs/cli

l   HTTPie套件基本用法,在Terminal執行:
http --help

http GET http://localhost:8000/

http PUT http://localhost:8000/items/21 name=timmy price=100 is_offer=true

http --json --auth <email>:<password> GET http://localhost:5000/api/v1/posts/

http --json --auth <email>:<password> POST http://localhost:5000/api/v1/posts/ "body=I'm adding a post from the *command line*."

三、使用click模組製作命令列說明

click模組

l   資料來源:
Python Click 模組製作好用的指令

l   hello.py
import click

@click.command()
# type
驗證參數、required必要參數
@click.option("-r", "--repeat", "repeat", help="Repeat N Times", type=int, required=True)
# default
預設參數、show_default顯示預設參數
@click.option("-t", "--to", "to", help="To Who", default="Stranger", show_default=True)
# greeting()
to@click.option()"to"必須一致,repeat亦同
def greeting(repeat, to):
    """Say Hello to Someone"""
    for i in range(0, repeat):
        print(f"Hello, {to}!")

if __name__ == "__main__":
    greeting()

l   hello.py使用方式如下:
python hello.py --help
python hello.py -r 6
python hello.py --repeat 6
python hello.py -r 6 -t Timmy

l   do_sum.py
import click

@click.command()
# option(nargs=2)
指定個數參數
# @click.option("-s", "--sum", "numbers", nargs=2, type=float, required=True)
# argument(nargs=-1)
不定個數參數
@click.argument("numbers", nargs=-1, type=float, required=True)
def do_sum(numbers):
    """Sum Numbers"""
    print(f"Sum: {sum(numbers)}")

if __name__ == "__main__":
    do_sum()

l   do_sum.py使用方式如下:
python do_sum.py --help
#
指定個數參數
python do_sum.py -s 6 21
#
不定個數參數
python do_sum.py 6 21

l   register.py
import click

@click.command()
#
prompt沒有提供字串內容,僅設定True,顯示--之後的字串,例如此例的Password
@click.option("-n", "--name", prompt="Your Name Please", required=True)
@click.option("-p", "--password", prompt=True, hide_input=True, confirmation_prompt=True)
def register(name, password):
    """Register and Say Hello to Someone"""
    click.echo(f"Hello, {name}")

if __name__ == "__main__":
    register()

l   register.py使用方式如下:
python register.py --help
python register.py
python register.py -n Timmy
python register.py -n Timmy -p my_password

l   shout.py
import click

@click.command()
# --shout
True--no-shoutFalse
@click.option("--shout/--no-shout", default=False)
def shout(shout):
    """Echo If You Were Noisy"""
    if shout:
        click.echo("You are noisy.")
    elif not shout:
        click.echo("You are quiet.")

if __name__ == "__main__":
    shout()

l   shout.py使用方式如下:
python shout.py --help
python shout.py
python shout.py --no-shout
python shout.py --shout

四、使用單元測試相關套件

unittest模組

l   資料來源與先備知識:
Python Tutorial 第六堂(1)使用 unittest 單元測試

l   setUp()tearDown()會在每個獨立測試之前、之後執行:
import unittest

class TestAddition(unittest.TestCase):
    def setUp(self):
        print("Setting Up the Test")
    def tearDown(self):
        print("Tearing Down the Test")
    def test_twoPlusTwo(self):
        total = 2 + 2
        self.assertEqual(4, total);

if __name__ == "__main__":
    unittest.main()

l   setUpClass()函式會在整個class中的所有測試開始前執行一次:
import requests, bs4, unittest

class TestWikipedia(unittest.TestCase):
    aBeaSou = None
    def setUpClass():
        global aBeaSou
        url = "http://en.wikipedia.org/wiki/Monty_Python"
        aBeaSou = bs4.BeautifulSoup(requests.get(url).text, "html.parser")
    def test_titleText(self):
        global aBeaSou
        pageTitle = aBeaSou.find("h1").get_text()
        self.assertEqual("Monty Python", pageTitle)
    def test_contentExists(self):
        global aBeaSou
        content = aBeaSou.find("div", {"id": "mw-content-text"})
        self.assertIsNotNone(content)

if __name__ == "__main__":
    # TestLoader()
加載測試實例,並將它們返回給測試套件
    suite = (unittest.TestLoader().loadTestsFromTestCase(TestWikipedia))
    # verbosity=2
為詳細模式,測試結果會顯示每個測試實例所有訊息
    unittest.TextTestRunner(verbosity=2).run(suite)

coverage套件

l   測試程式覆蓋率工具,這個工具附帶一個命令列腳本可在啟用程式碼覆蓋率工具的情況下執行任何Python App,也可以用程式來啟動覆蓋率引擎:
pip install coverage

l   coverage套件文件:
https://coverage.readthedocs.io/en/7.2.7/

l   coverage套件基本使用架構如下:
import coverage
#
啟動覆蓋率引擎,branch=True選項會啟用分支覆蓋分析,追蹤有哪幾行程式碼被執行,也會確認每一個條件式是否已經執行了TrueFalse兩種情況
#
僅包含app資料夾的程式,如果沒有include選項,在虛擬環境安裝的所有擴充套件以及測試程式本身都會被納入覆蓋率報告,讓報告有許多雜訊
COV = coverage.coverage(branch=True, include="app/*")
COV.start()

import unittest
# discover()
找到指定目錄下的所有測試程式
tests = unittest.TestLoader().discover("tests")
unittest.TextTestRunner(verbosity=2).run(tests)

COV.stop()
COV.save()

#
Terminal顯示測試報告
print("Coverage Summary:")
COV.report()

#
在當前目錄的tmp/coverage資料夾,儲存HTML版本的測試報告
import os
basedir = os.path.abspath(os.path.dirname(__file__))
covdir = os.path.join(basedir, "tmp/coverage")
COV.html_report(directory=covdir)
print(f"HTML version: file://{covdir}/index.html")

COV.erase()

l   套用coverage套件基本使用架構的測試範例如下:
# app/addition.py
class Addition():
    def numberOnePlusTwo(num1, num2):
        total = num1 + num2
        return total

# tests/test_addition.py
import unittest
from app.addition import Addition

class TestAddition(unittest.TestCase):
    def setUp(self):
        print("Setting Up the Test")
    def tearDown(self):
        print("Tearing Down the Test")
    def test_onePlusTwo(self):
        total = Addition.numberOnePlusTwo(6, 21)
        self.assertEqual(27, total)

if __name__ == "__main__":
    unittest.main()

五、使用python-dotenv套件匯入環境變數

python-dotenv套件

l   安裝python-dotenv套件:
pip install python-dotenv

l   python-dotenv套件文件:
https://pypi.org/project/python-dotenv/

l   python-dotenv套件基本用法:
# .env
檔內容
DOMAIN=example.org
ADMIN_EMAIL=admin@${DOMAIN}
ROOT_URL=${DOMAIN}/app

# .py
檔從.env檔匯入環境變數
import os
from dotenv import load_dotenv

dotenv_path = os.path.join(os.path.dirname(__file__), ".env")
print(os.getenv("ROOT_URL"))
à None

if os.path.exists(dotenv_path):
    load_dotenv(dotenv_path)
print(os.getenv("ROOT_URL"))
à example.org/app

沒有留言:

張貼留言