2

Python 基于多环境的配置方式

 1 year ago
source link: https://yanbin.blog/python-multi-envs-configurations/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Python 基于多环境的配置方式

2023-01-13 | 阅读(4)

部署到不同环境的应用会使用到各自的配置,如 Dev, QA, Stg, Prod 有自己的数据库等资源。Spring Boot 可采用 Profile 对应不同的环境,不同 Profile 选择自己的配置文件 application-${profile}.properties。本人还是偏爱在同一个文件中分组配置,容易查错与编辑,类如在 application.properties 文件中以下面的方式

db.host=aaa
%dev.db.host=bbb
%prod.db.host=ccc 

那么在 Python 的项目中应该如何针对不同环境进行配置呢?大概有以下几种

  1. 不同环境的 Config 类
  2. YAML 文件
  3. TOML 文件
  4. JSON 文件
  5. INI 文件
  6. dotenv(.env) 文件

第一种方式是本人推荐的,其他的方式只是在不同格式的配置文件中,按环境组织不同的配置值,其他方式的不同配置读入内存中基本是体现为字典变量。在 Python 配置中要支持像配置的 placeholder(像 ${host} 还需自己实现。

不同环境的 Config 类

在同一个文件中配置,方便用点号引用

import os
class Config:
    DB_HOST = "qa.example.com"
    DB_USER = "sa"
class DevConfig(Config):
    DB_HOST = "dev.example.com"
class QAConfig(Config):
class ProdConfig(Config):
    DB_HOST = "prod.example.com"
mapping = {
    'dev': DevConfig,
    'qa': QAConfig,
    'prod': ProdConfig
APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
config = mapping[APP_ENV]()
from config import config
print(config.DB_HOST)

配置环境变量 APP_ENVdev, qa, 或 prod 会输出不同的 DB_HOST 值

YAML 文件

需安装依赖 pyyaml

pip install pyyaml

config.yml 文件配置

default:
  db_host: qa.example.com
  db_host: dev.example.com
prod:
  db_host: prod.example.com
import yaml
from yaml import Loader
with open("config.yml") as ymlfile:
    cfg = yaml.load(ymlfile, Loader)
print(type(cfg))
print(cfg)

cfg 是一个字典,所以上面的输出为

<class 'dict'>
{'default': {'db_host': 'qa.example.com'}, 'dev': {'db_host': 'dev.example.com'}, 'qa': None, 'prod': {'db_host': 'prod.example.com'}}

如果结合环境变量 APP_ENV 从 cfg 中获取配置值

import os
APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
print(cfg.get(APP_ENV, "default")["db_host"])

每次取值有些麻烦, 更高级一点的玩法是让 YAML 序列化为一个自定义对象,然后在自定义类中做文章,如新的 config.yml 配置中要告诉对应的类名

--- !Config
default:
  db_host: qa.example.com
  db_host: dev.example.com
prod:
  db_host: prod.example.com

然后定义 Config 类,并使用相应的配置项

import yaml
from yaml import Loader
import os
APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
class Config(yaml.YAMLObject):
    yaml_tag = u'!Config'
    def __int__(self, default, dev, qa, prod):
        self.default = default
        self.dev = dev
        self.qa = qa
        self.prod = prod
    def __getitem__(self, item):
        env_conf = getattr(self, APP_ENV) if hasattr(self, APP_ENV) else self.default
        env_conf = env_conf if env_conf else {}
        return env_conf[item] if item in env_conf else self.default[item]
with open("config.yml") as ymlfile:
    cfg = yaml.load(ymlfile, Loader)
print(type(cfg))
print(cfg["db_host"])

上面的代码输出

<class '__main__.Config'>
dev.example.com

 再改变 APP_ENV 环境变量为 qa 和 prod 时对应的 cfg["db_host"] 的值分别为

qa.example.com
prod.example.com

TOML 文件

TOML(Tom's Obvious Minimal Language),初看它的格式像 ini 文件,其实它对 ini 格式强悍许多,支持丰富的数据类型,如布尔型,整数,浮点数,时间,日期,列表和字典等,下面是官方的一个配置样例

# This is a TOML document
title = "TOML Example"
[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00
[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
data = [ ["delta", "phi"], [3.14] ]
temp_targets = { cpu = 79.5, case = 72.0 }
[servers]
[servers.alpha]
ip = "10.0.0.1"
role = "frontend"
[servers.beta]
ip = "10.0.0.2"
role = "backend"

Python 的项目管理工具 Poetry 就是用 pyproject.toml 文件来管理依赖配置的。

使用 toml 一般安装

pip install toml

把前面的 config.yml 文件转换为 config.toml 文件,内容如下

[default]
db_host="qa.example.com"
[dev]
db_host= "dev.example.com"
[prod]
db_host="prod.example.com"

加载该 toml 文件

import toml
with open('config.toml') as tomlfile:
    cfg = toml.load(tomlfile)
print(type(cfg))
print(cfg)

<class 'dict'>
{'default': {'db_host': 'qa.example.com'}, 'dev': {'db_host': 'dev.example.com'}, 'qa': {}, 'prod': {'db_host': 'prod.example.com'}}

要取随 APP_ENV 环境而变的 db_host 的话,代码可实现为

import toml
import os
APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
class Config:
    def __init__(self, default, dev, qa, prod):
        self.default = default
        self.dev = dev
        self.qa = qa
        self.prod = prod
    def __getitem__(self, item):
        env_conf = getattr(self, APP_ENV) if hasattr(self, APP_ENV) else self.default
        env_conf = env_conf if env_conf else {}
        return env_conf[item] if item in env_conf else self.default[item]
with open('config.toml') as tomlfile:
    cfg = Config(**toml.load(tomlfile))
print(cfg['db_host'])

变更 APP_ENV 环境变量的值为 qa, prod, 会输出以下相应的值

qa.example.com
prod.example.com

Config 类的 __init____getitem__ 方法实现与前面的完全一样。

TOML 配置文件的表现力很丰富,更强大的功能还有待于日后去发掘。

JSON 文件

上面相应的配置文件变成 config.json 就是

  "default": {
    "db_host": "qa.example.com"
  "dev": {
    "db_host": "dev.example.com"
  "qa": {},
  "prod": {
    "db_host": "prod.example.com"
import json
import os
with open("config.json") as jsonfile:
    cfg = json.load(jsonfile)
print(type(cfg))
print(cfg)
APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
env_conf = cfg.get(APP_ENV)
env_conf = env_conf if env_conf else cfg['default']
print(env_conf['db_host'])

<class 'dict'>
{'default': {'db_host': 'qa.example.com'}, 'dev': {'db_host': 'dev.example.com'}, 'qa': {}, 'prod': {'db_host': 'prod.example.com'}}
qa.example.com

切换 APP_ENV 环境变量测试不同环境下的 db_host 值

json.load() 方法也能由 JSON 格式数据反序列化为一个自定义的对象,直接用 object_hook 参数把有嵌套的 JSON 转换成一个自定义对象可就不那么容易了。但通过自定义的 __init__ 方法就和前面 YAML 的例子差不多了

import json
class Config:
    # 实现代码与前方 TOML 中的 Config 完全相同,故省略
with open("config.json") as jsonfile:
    cfg = Config(**json.load(jsonfile))
print(cfg["db_host"])

或者整体 JSON 对象可转换为一个 SimpleNamespace

from types import SimpleNamespace
with open("config.json") as jsonfile:
    namespace = json.load(jsonfile, object_hook=lambda d: SimpleNamespace(**d))
    # 再把 namespace 转换为 Config 对象

INI 文件

ini 文件以前广泛应用在 Windows 中作为配置文件的格式,Python 也内置了对它的支持,格式上有点像 TOML 但它不支持嵌套类型。这里只提下 INI 文件的简单读取

config.ini

[default]
db_host=qa.example.com
[dev]
db_host=dev.example.com
[prod]
db_host=prod.example.com
import configparser
cfg = configparser.ConfigParser()
cfg.read("config.ini")
print(type(cfg))
host = cfg['dev']['db_host']
print(host)

<class 'configparser.ConfigParser'>
dev.example.com

由于只有一个层次的 Section 系列,不易于扩展,实际中应用较为狭窄,不作细究。

dotenv(.env) 文件

基本思路是把 .env 文件中的配置转换为环境变量,可由 os.environ().get(key) 获得,相当于 Linux 下的环境配置 env.sh

export DOMAIN=example.org
export ADMIN_EMAIL=admin@${DOMAIN}

source env.sh

相应的 DOMAIN 和 ADMIN_EMAIL 就出现在了 env 列出的环境变量中

Python 的 dotenv 有两个实现库

第一个是 python-dotenv, 安装

pip install python-dotenv

我们在当前目录中创建一个 .env 文件,其中内容为

# Development settings
DOMAIN=example.org
ADMIN_EMAIL=admin@${DOMAIN}
ROOT_URL=${DOMAIN}/app
from dotenv import load_dotenv
import os
load_dotenv()
print(os.environ.get("DOMAIN"))
print(os.environ.get("ADMIN_EMAIL"))

example.org
[email protected]

load_dotenv() 可以指定不同的文件, 例如采用基于环境区分的文件命名

  • .env      -- 默认的配置
  • .env_dev
  • .env_qa
  • .env_prod
from dotenv import load_dotenv
import os
load_dotenv('.env')           # 先加载默认的 .env 文件
APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
load_dotenv(f'.env_{APP_ENV}', override=True) # 再加载环境相关的,
print(os.environ)

先加载默认的 .env, 再加载环境相关的 .env_dev,这样 .env_dev 中的相同属性会覆盖 .env 中的配置。

还有一个 django 的实现 django-environ, 但其中夹带了太多的私货, 如 environ.Env() 中有一些特定的配置项(db_url, cache_url 等),严格来说,它算不上通用 dotenv 实现。它加载 .env 文件时的行为与 python-dotenv 类似,如

import environ
import os
env = environ.Env()
env.read_env()  # 加载 .env 文件
APP_ENV = os.environ.get('APP_ENV', 'dev').lower()
env.read_env(f'.env_{APP_ENV}', overwrite=True)  # 加载环境相关的,如 .env_qa
print(os.environ)
print(os.environ["ADMIN_EMAIL"])
print(env.str("ADMIN_EMAIL"))

django-environ 中配置的值可以有类型,如 str, bool, int 等。它也像 python-dotenv 一样把 .env 文件中配置加到 os.environ 中去,因此既可通过 os.environ 来获取 .env 文件中配置的值,也能用它自己专有的 environ.Env() 的方式取得值。

另外,比起 python-dotenv 弱的地方就是它不支持 placeholder 的解析,.env 配置中的 ${DOMAIN} 将会被原样输出。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK