背景
- 在許多情況下,應用程式可能需要一些外部設定或配置,例如金鑰、資料庫憑據、電子郵件服務憑據等。
- 大多數這些設定都是可變的(可以更改),例如資料庫 URL,很多可能是敏感資料,比如密碼
- 出於這個原因,通常在應用程式讀取的環境變數中提供它們
Pydantic Settings
- Pydantic 提供了一個很好的實用程式來處理環境變數的設定
- 從 Pydantic 匯入 BaseSettings 並建立一個子類,非常類似於 Pydantic 的 BaseModel
- 與 Pydantic Model 一樣,可以使用型別註釋和預設值宣告類屬性
- 可以使用和 Pydantic Model 的所有相同驗證功能和工具,例如不同的資料型別和使用 Field()
#!usr/bin/env python # -*- coding:utf-8 _*- """ # author: 小菠蘿測試筆記 # blog: https://www.cnblogs.com/poloyy/ # time: 2021/10/9 7:25 下午 # file: 52_settings_env.py """ import os import uvicorn from fastapi import FastAPI from pydantic import BaseSettings class Settings(BaseSettings): app_name: str = "Awesome API" admin_email: str items_per_user: int = 50 settings = Settings() app = FastAPI() @app.get("/info") async def info(): return { "app_name": settings.app_name, "admin_email": settings.admin_email, "items_per_user": settings.items_per_user, }
- 然後,當建立 Settings 該類的例項時,Pydantic 將以不區分大小寫的方式讀取環境變數
- 因此,仍會為屬性 app_name 讀取為大寫變數 APP_NAME
- 接下來它將轉換和驗證資料
- 因此,當使用該 settings 物件時,將擁有宣告的型別的資料(例如 items_per_user 是 int)
執行 uvicorn 伺服器
要為單個命令設定多個環境變數,只需用空格分隔它們,並將它們全部放在命令之前
ADMIN_EMAIL="deadpool@example.com" APP_NAME="ChimichangApp" uvicorn main:app
訪問 /info 介面
Settings 跨模組呼叫
config.py
from pydantic import BaseSettings class Settings(BaseSettings): app_name: str = "Awesome API" admin_email: str items_per_user: int = 50 settings = Settings()
main.py
from fastapi import FastAPI from .config import settings app = FastAPI() @app.get("/info") async def info(): return { "app_name": settings.app_name, "admin_email": settings.admin_email, "items_per_user": settings.items_per_user, }
Settings 在依賴項中
前言
- 在某些情況下,提供依賴項的 Settings 會有用,而不是讓全域性物件擁有可隨處使用的 Settings
- 在測試期間會有用,因為使用自定義 Settings 覆蓋依賴項非常容易
config.py
from pydantic import BaseSettings class Settings(BaseSettings): app_name: str = "Awesome API" admin_email: str items_per_user: int = 50
這裡不建立預設例項 settings = Settings()
main.py
from fastapi import FastAPI, Depends from functools import lru_cache from .config import Settings app = FastAPI() @lru_cache def get_settings(): return Settings @app.get("/info") async def info(settings: Settings = Depends(get_settings)): return { "app_name": settings.app_name, "admin_email": settings.admin_email, "items_per_user": settings.items_per_user, }
測試上述介面
from fastapi.testclient import TestClient from .config import Settings from .main import app, get_settings client = TestClient(app) # 依賴覆蓋,為 Settings 物件設定一個新的 admin_email 值 def get_settings_override(): return Settings(admin_email="testing_admin@example.com") app.dependency_overrides[get_settings] = get_settings_override def test_app(): response = client.get("/info") data = response.json() assert data == { "app_name": "Awesome API", "admin_email": "testing_admin@example.com", "items_per_user": 50, }
命令列執行
> pytest 53_settings_test.py ============================================================================================================ test session starts ============================================================================================================ platform darwin -- Python 3.9.5, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 rootdir: /Users/polo/Downloads/FastAPI_project plugins: anyio-3.3.2 collected 1 item 53_settings_test.py . [100%] ============================================================================================================= 1 passed in 0.30s =============================================================================================================
使用 .env 檔案
背景
如果有會經常變化的設定項,也許在不同的環境中,將它們放在一個檔案中,然後從檔案中讀取它們,就好像它們是環境變數一樣
這些環境變數通常放在一個檔案 .env 中,該檔案稱為“dotenv”
tips
- 以點 (.) 開頭的檔案是類 Unix 系統(如 Linux 和 macOS)中的隱藏檔案
- 但是 dotenv 檔案實際上不必具有那個確切的檔名
- Pydantic 支援使用外部庫讀取這型別的檔案
安裝第三方庫
pip install python-doten
.env 檔案
ADMIN_EMAIL="xiaopolo@example.com" APP_NAME="小菠蘿"
config.py 檔案
from pydantic import BaseSettings class Settings(BaseSettings): app_name: str = "Awesome API" admin_email: str items_per_user: int = 50 class Config: # 設定需要識別的 .env 檔案 env_file = ".env"
lru_cache
背景
繼上面的栗子,讀取 .env 檔案可能是一件代價高昂(緩慢)的操作
從效能角度出發,肯定希望只讀取一次,後續每個請求可以重複使用同一個 Settings 物件,這樣就只會讀取一次 .env 檔案
def get_settings(): return Settings()
上述程式碼,如果作為請求的依賴項,那麼每次請求進來,都會建立一個 Settings 物件,然後讀取一次 .env 檔案,這不是我們希望的
@lru_cache
如果加上了 @lru_cache 那麼 get_settings 只會在第一次呼叫的時候執行一次,然後 Settings 物件也只會建立一次,.env 檔案也只會讀取一次
from functools import lru_cache from fastapi import Depends, FastAPI from . import config app = FastAPI() @lru_cache() def get_settings(): return config.Settings() @app.get("/info") async def info(settings: config.Settings = Depends(get_settings)): return { "app_name": settings.app_name, "admin_email": settings.admin_email, "items_per_user": settings.items_per_user, }
對於後續請求的依賴項中的 get_settings() 的任何後續呼叫,它不會執行 get_settings() 的內部程式碼並建立新的 Settings 物件,而是返回與第一次呼叫時返回的相同物件
lru_cache 技術細節
- @lru_cache() 修改它修飾的函式返回與第一次返回相同的值,而不是再次執行函式內部程式碼
- 因此,它下面的函式將針對每個引數組合執行一次
- 然後,每當使用完全相同的引數組合呼叫函式時,每個引數組合返回相同的值將一次又一次地使用
- 在請求依賴項 get_settings() 的情況下,該函式沒有引數,所以它總是返回相同的值
- 這樣,它的行為就好像它只是一個全域性變數
- 但是因為它使用了一個依賴函式,所以可以很容易地覆蓋它進行測試
- @lru_cache() 是 functools 的一部分,它是 Python 標準庫的一部分
- 使用 @lru_cache() 可以避免為每個請求一次又一次地讀取 .env 檔案,同時可以在測試期間覆蓋它的值
有引數的函式的栗子
@lru_cache() def say_hi(name: str, salutation: str = "Ms."): print(123) return f"Hello {salutation} {name}" print(say_hi(name="Camila")) print(say_hi(name="Camila")) print(say_hi(name="Rick", salutation="Mr.")) print(say_hi(name="Rick", salutation="Mr.")) print(say_hi(name="Camila")) print(say_hi(name="Rick", salutation="Mr."))
執行結果
123 Hello Ms. Camila Hello Ms. Camila 123 Hello Mr. Rick Hello Mr. Rick Hello Ms. Camila Hello Mr. Rick
使用完全相同的引數呼叫函式時,直接返回結果而不會執行釐米的程式碼
原理圖