python 服务端项目配置文件最佳实践

1,958 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情


python 服务端项目配置文件最佳实践

前言

我们在编写服务端代码或搭建服务器框架的时候,经常会考虑配置文件该怎么写?写在哪里?什么格式的?json?ini?yaml?环境变量?写完之后该怎么取呢?今天这篇就以sanic为例给大家带来一个我认为的最佳实践

常见方案

翻开sanic官方文档,配置文件是那一章节,可以看到官方有很多配置文件的加载方案,包括:

  • dict格式数据:{"a": 1}

  • python 文件格式数据: a = 1

  • 通过类的方式:

    class MyConfig:
        A = 1
        B = 2
    

然后再通过:app.update_config(...)的方法加载不同的配置文件,在程序中使用的时候也是通过app.config.xx的方式来获取配置

对应不同的服务端框架也有各自不同的配置文件载入方式,但都存在常见的几个问题:

  • 灵活。配置文件可以复杂,也可以简单,可以有一层,也可以多层
  • 多种格式。可以选择使用ini格式、.env格式、json格式、yaml格式等多种格式
  • 代码提示。可以在项目的任何地方方便的获取配置,无需在去配置中去查看某个配置项到底是咋拼的来着
  • 环境变量。可以选择加载环境变量的值,且支持类型的转换

pydantic配置

简单介绍

pydantic的顶顶大名大家应该都了解了,可以用来做参数校验、类型的Schema生成等,但是还要一个同样强大的功能就是做项目的配置管理

官方文档:Settings management

它具有丰富的类型,如:

  • 使用BaseModel进行声明嵌套的结构
  • 使用RedisDsn载入redis的连接信息
  • 使用PostgresDsn载入pgsql的连接信息
  • 使用PyObject载入python的函数等
  • 更多类型

支持以全大写的方式从环境变量中自动获取参数,也就是说,声明变量的时候可以是小写的,但是获取环境变量的时候确是大写的,也支持环境变量的嵌套结构,下面通过实例去演示一下:

from pydantic import (
    BaseModel,
    BaseSettings,
    PyObject,
    RedisDsn,
    PostgresDsn,
    AmqpDsn,
    Field,
)
​
​
class SubModel(BaseModel):
    foo = 'bar'
    apple = 1
​
​
class Settings(BaseSettings):
    auth_key: str
    api_key: str = Field(..., env='my_api_key')
    
    # 各种连接信息
    redis_dsn: RedisDsn = 'redis://user:pass@localhost:6379/1'
    pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar'
    amqp_dsn: AmqpDsn = 'amqp://user:pass@localhost:5672/'
    
    # 传递一个python对象
    special_function: PyObject = 'math.cos'# 数组结构的数据:["foo.com", "bar.com"]
    domains: set[str] = set()
​
    # 嵌套结构的数据:{"foo": "x", "apple": 1}
    more_settings: SubModel = SubModel()
​
​
print(Settings().dict())
"""
{
    'auth_key': 'xxx',
    'api_key': 'xxx',
    'redis_dsn': RedisDsn('redis://user:pass@localhost:6379/1', ),
    'pg_dsn': PostgresDsn('postgres://user:pass@localhost:5432/foobar', ),
    'amqp_dsn': AmqpDsn('amqp://user:pass@localhost:5672/', scheme='amqp',
user='user', password='pass', host='localhost', host_type='int_domain',
port='5672', path='/'),
    'special_function': <built-in function cos>,
    'domains': set(),
    'more_settings': {'foo': 'bar', 'apple': 1},
}
"""

多种配置读取方式

使用pydantic管理配置可以很灵活的使用不同的格式来管理配置,可以通过自定义来源的方式来使用,如:

from pydantic import BaseSettings
from pydantic.env_settings import SettingsSourceCallable
​
class SettingsBase(BaseSettings):
    """
    项目设置的基类
    """
​
    class Config:
        env_file = getpath_by_root("../.env")
        env_file_encoding = "utf-8"
        env_nested_delimiter = "__"
​
        @classmethod
        def customise_sources(
            cls,
            init_settings: SettingsSourceCallable,
            env_settings: SettingsSourceCallable,
            file_secret_settings: SettingsSourceCallable,
        ) -> tuple[SettingsSourceCallable, ...]:
            ...

在你的设置类里面定义配置类Config,可以进行灵活的配置,其中类方法customise_sources就是加载不同来源的地方

环境变量

定义了三个变量:

  • env_file:定义了Dotenv格式的环境变量文件的存储位置
  • env_file_encoding:定义环境变量文件的编码格式
  • env_nested_delimiter:定义了嵌套的结构用环境变量表示的时候怎么进行分割

这样既可以通过直接设置系统环境变量的方式来加载配置,也可以通过.env环境变量文件的方式来加载配置

json 配置文件

我们可以使用常用的json格式来加载我们的配置,使用方法如下:

class CustomSettingsSource(object):
    """
    自定义的配置文件来源基类
    """
​
    def __init__(self, path: Path):
        self.path = path
​
    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(path={self.path!r})"
    
class JsonSettingsSource(CustomSettingsSource):
    """
    json文件来源导入配置项
    """
​
    def __call__(self, settings: BaseSettings) -> dict[str, Any]:
        encoding = settings.__config__.env_file_encoding
        return json.loads(self.path.read_text(encoding))
    
    ......
    
    # 默认的设置
    default_settings = {
        init_settings,
        env_settings,
        file_secret_settings,
    }
​
    # json 配置文件
    json_file = getpath_by_root("../settings.json")
    if json_file.exists():
        json_settings_source = JsonSettingsSource(json_file)
        default_settings.add(json_settings_source)
    
    ......
    

customise_sources中我们先判断了json文件的位置,如果存在则使用JsonSettingsSource类把json文件内容加载进去

ini、yaml配置文件

这两种就和上面的json类似了,通过各自的类去把文件加载成字典的格式,然后如果路径存在则加载对应的文件。

示例代码

这里代码是一个配置的基类代码:

import json
from configparser import ConfigParser
from pathlib import Path
from typing import Anyimport yaml
from pydantic import BaseSettings
from pydantic.env_settings import SettingsSourceCallable
​
from server.util.path import getpath_by_root
​
​
class CustomSettingsSource(object):
    """
    自定义的配置文件来源基类
    """
​
    def __init__(self, path: Path):
        self.path = path
​
    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(path={self.path!r})"
​
​
class JsonSettingsSource(CustomSettingsSource):
    """
    json文件来源导入配置项
    """
​
    def __call__(self, settings: BaseSettings) -> dict[str, Any]:
        encoding = settings.__config__.env_file_encoding
        return json.loads(self.path.read_text(encoding))
​
​
class IniSettingsSource(CustomSettingsSource):
    """
    ini文件来源导入配置项
    """
​
    def __call__(self, settings: BaseSettings) -> dict[str, Any]:
        encoding = settings.__config__.env_file_encoding
        parser = ConfigParser()
        parser.read(self.path, encoding)
        return getattr(parser, "_sections", {}).get("settings")
​
​
class YamlSettingsSource(CustomSettingsSource):
    """
    ini文件来源导入配置项
    """
​
    def __call__(self, settings: BaseSettings) -> dict[str, Any]:
        encoding = settings.__config__.env_file_encoding
        return yaml.safe_load(self.path.read_text(encoding))
​
​
class SettingsBase(BaseSettings):
    """
    项目设置的基类
    """
​
    class Config:
        env_file = getpath_by_root("../.env")
        env_file_encoding = "utf-8"
        env_nested_delimiter = "__"
​
        @classmethod
        def customise_sources(
            cls,
            init_settings: SettingsSourceCallable,
            env_settings: SettingsSourceCallable,
            file_secret_settings: SettingsSourceCallable,
        ) -> tuple[SettingsSourceCallable, ...]:
            """
            自定义配置来源
            Args:
                init_settings: 初始化设置
                env_settings:环境变量设置
                file_secret_settings:加密文件设置
​
            Returns:
​
            """
            # 默认的设置
            default_settings = {
                init_settings,
                env_settings,
                file_secret_settings,
            }
​
            # json 配置文件
            json_file = getpath_by_root("../settings.json")
            if json_file.exists():
                json_settings_source = JsonSettingsSource(json_file)
                default_settings.add(json_settings_source)
​
            # ini配置文件
            ini_file = getpath_by_root("../settings.ini")
            if ini_file.exists():
                ini_settings_source = IniSettingsSource(ini_file)
                default_settings.add(ini_settings_source)
​
            # yaml配置文件
            yaml_file = getpath_by_root("../settings.yaml")
            if yaml_file.exists():
                yaml_settings_source = YamlSettingsSource(yaml_file)
                default_settings.add(yaml_settings_source)
​
            return tuple(default_settings)

里面分别配置了默认值、环境变量、加密文件、json文件、ini配置文件、yaml配置文件多种来源方式,并设置优先级

之后可以再其他文件中继承这个类来使用:

from pydantic import Field, RedisDsn
​
from ..entity.enum import RunModeEnum
from .base import SettingsBase
from .db import PGDBConfig
​
​
class Settings(SettingsBase):
    # 监听的地址和端口
    host: str = Field(default="0.0.0.0")
    port: int = Field(default=8080)
​
    # 模式 dev 或 production
    mode: RunModeEnum = Field(default=RunModeEnum.DEV)
​
    # 是否是调试模式 生产模式强制为False
    debug: bool = Field(default=True)
​
    # 是否开启自动重载 生产模式强制为False
    auto_reload = Field(default=True)
    
    # 数据库配置
    db: dict[str, PGDBConfig] = Field()

db.py:

from pydantic import BaseModel, Field
​
​
class PGDBConfig(BaseModel):
    # 数据库名字
    database: str = Field()
​
    # 数据库地址
    host: str = Field(default="127.0.0.1")
​
    # 数据库端口
    port: int = Field(default=5432)
​
    # 数据库用户名
    user: str = Field(default="postgres")
​
    # 数据库密码
    password = Field(default="")
​
    # 数据库连接池大小
    maxsize = Field(default=10, cast=int)
​
    # 配置
    server_settings = Field(default={"jit": "off"})

这样可以更灵活的加载配置

结语

这篇文章主要以sanic为基础介绍了再项目中加载配置文件的优雅的一种方式,仓促之下没有说的太详细,后续会再修改修改,尽量配合实例更通俗易懂一点,希望大家能够有所收获!!!