FastAPI(64)- Settings and Environment Variables 配置項和環境變數

小菠蘿測試筆記發表於2021-10-10

背景

  • 在許多情況下,應用程式可能需要一些外部設定或配置,例如金鑰、資料庫憑據、電子郵件服務憑據等。
  • 大多數這些設定都是可變的(可以更改),例如資料庫 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

使用完全相同的引數呼叫函式時,直接返回結果而不會執行釐米的程式碼

 

原理圖

  

相關文章