下面推荐一个 environs 库,利用它我们可以轻松地设置各种类型的环境变量。
安装:
pip3 install environs
安装之后,我们再来体验一下使用 environs 来设置环境变量的方式。
from environs import Env
env = Env()
VAR1 = env.int('VAR1', 1)
VAR2 = env.float('VAR2', 5.5)
VAR3 = env.list('VAR3')
这里 environs 直接提供了 int、float、list 等方法,我们就不用再去进行类型转换了。
与此同时,设置环境变量的方式也有所变化:
export VAR1=1
export VAR2=2.3
export VAR3=1,2
这里 VAR3 是列表,我们可以直接用逗号分隔开来。
打印结果如下:
1
2.3
['1', '2']
下面我们再看一个官方示例,这里示例了一些常见的用法。
首先我们来定义一些环境变量,如下:
export GITHUB_USER=sloria
export MAX_CONNECTIONS=100
export SHIP_DATE='1984-06-25'
export TTL=42
export ENABLE_LOGIN=true
export GITHUB_REPOS=webargs,konch,ped
export COORDINATES=23.3,50.0
export LOG_LEVEL=DEBUG
这里有字符串、有日期、有日志级别、有字符串列表、有浮点数列表、有布尔。
我们来看下怎么获取,写法如下:
from environs import Env
env = Env()
env.read_env() # read .env file, if it exists
# required variables
gh_user = env("GITHUB_USER") # => 'sloria'
secret = env("SECRET") # => raises error if not set
# casting
max_connections = env.int("MAX_CONNECTIONS") # => 100
ship_date = env.date("SHIP_DATE") # => datetime.date(1984, 6, 25)
ttl = env.timedelta("TTL") # => datetime.timedelta(0, 42)
log_level = env.log_level("LOG_LEVEL") # => logging.DEBUG
# providing a default value
enable_login = env.bool("ENABLE_LOGIN", False) # => True
enable_feature_x = env.bool("ENABLE_FEATURE_X", False) # => False
# parsing lists
gh_repos = env.list("GITHUB_REPOS") # => ['webargs', 'konch', 'ped']
coords = env.list("COORDINATES", subcast=float) # => [23.3, 50.0]
通过观察代码可以发现它提供了这些功能:
通过 env 可以设置必需定义的变量,如果没有定义,则会报错。
通过 date、timedelta 方法可以对日期或时间进行转化,转成 datetime.date 或 timedelta 类型。
通过 log_level 方法可以对日志级别进行转化,转成 logging 里的日志级别定义。
通过 bool 方法可以对布尔类型变量进行转化。
通过 list 方法可以对逗号分隔的内容进行 list 转化,并可以通过 subcast 方法对 list 的每个元素进行类型转化。
可以说有了这些方法,定义各种类型的变量都不再是问题了。
支持类型
总的来说,environs 支持的转化类型有这么多:
env.str
env.bool
env.int
env.float
env.decimal
env.list (accepts optional subcast keyword argument)
env.dict (accepts optional subcast keyword argument)
env.json
env.datetime
env.date
env.timedelta (assumes value is an integer in seconds)
env.url
env.uuid
env.log_level
env.path (casts to a pathlib.Path)
这里 list、dict、json、date、url、uuid、path 个人认为都还是比较有用的,另外 list、dict 方法还有一个 subcast 方法可以对元素内容进行转化。
对于 dict、url、date、uuid、path 这里我们来补充说明一下。
下面我们定义这些类型的环境变量:
export VAR_DICT=name=germey,age=25
export VAR_JSON='{"name": "germey", "age": 25}'
export VAR_URL=https://cuiqingcai.com
export VAR_UUID=762c8d53-5860-4d5d-81bc-210bf2663d0e
export VAR_PATH=/var/py/env
需要注意的是,DICT 的解析,需要传入的是逗号分隔的键值对,JSON 的解析是需要传入序列化的字符串。
解析写法如下:
from environs import Env
env = Env()
VAR_DICT = env.dict('VAR_DICT')
print(type(VAR_DICT), VAR_DICT)
VAR_JSON = env.json('VAR_JSON')
print(type(VAR_JSON), VAR_JSON)
VAR_URL = env.url('VAR_URL')
print(type(VAR_URL), VAR_URL)
VAR_UUID = env.uuid('VAR_UUID')
print(type(VAR_UUID), VAR_UUID)
VAR_PATH = env.path('VAR_PATH')
print(type(VAR_PATH), VAR_PATH)
运行结果:
<class 'dict'> {'name': 'germey', 'age': '25'}
<class 'dict'> {'name': 'germey', 'age': 25}
<class 'urllib.parse.ParseResult'> ParseResult(scheme='https', netloc='cuiqingcai.com', path='', params='', query='', fragment='')
<class 'uuid.UUID'> 762c8d53-5860-4d5d-81bc-210bf2663d0e
<class 'pathlib.PosixPath'> /var/py/env
可以看到,它分别给我们转化成了 dict、dict、ParseResult、UUID、PosixPath 类型了。
在代码中直接使用即可。
文件读取
如果我们的一些环境变量是定义在文件中的,environs 还可以进行读取和加载,默认会读取本地当前运行目录下的 .env 文件。
示例如下:
from environs import Env
env = Env()
env.read_env()
APP_DEBUG = env.bool('APP_DEBUG')
APP_ENV = env.str('APP_ENV')
print(APP_DEBUG)
print(APP_ENV)
下面我们在 .env 文件中写入如下内容:
APP_DEBUG=false
APP_ENV=prod
运行结果:
False
prod
当然我们也可以自定义读取的文件,如 .env.test 文件,内容如下:
APP_DEBUG=false
APP_ENV=test
代码调整:
from environs import Env
env = Env()
env.read_env(path=’.env.test’)
APP_DEBUG = env.bool(‘APP_DEBUG’)
APP_ENV = env.str(‘APP_ENV’)
复制代码
这里就通过 path 传入了定义环境变量的文件路径即可。
前缀处理
environs 还支持前缀处理,一般来说我们定义一些环境变量,如数据库的连接,可能有 host、port、password 等,但在定义环境变量的时候往往会加上对应的前缀,如 MYSQL_HOST、MYSQL_PORT、MYSQL_PASSWORD 等,但在解析时,我们可以根据前缀进行分组处理,见下面的示例:
复制代码
export MYAPP_HOST=lolcathost
export MYAPP_PORT=3000
with env.prefixed(“MYAPP_”):
host = env(“HOST”, “localhost”) # => ‘lolcathost’
port = env.int(“PORT”, 5000) # => 3000
nested prefixes are also supported:
export MYAPP_DB_HOST=lolcathost
export MYAPP_DB_PORT=10101
with env.prefixed(“MYAPP_”):
with env.prefixed(“DB_”):
db_host = env(“HOST”, “lolcathost”)
db_port = env.int(“PORT”, 10101)
复制代码
可以看到这里通过 with 和 priefixed 方法组合使用即可实现分区处理,这样在每个分组下再赋值到一个字典里面即可。
合法性验证
有些环境变量的传入是不可预知的,如果传入一些非法的环境变量很可能导致一些难以预料的问题。比如说一些可执行的命令,通过环境变量传进来,如果是危险命令,那么会非常危险。
所以在某些情况下我们需要验证传入的环境变量的有效性,看下面的例子:
复制代码
export TTL=-2
export NODE_ENV=‘invalid’
export EMAIL=’_’
from environs import Env
from marshmallow.validate import OneOf, Length, Email
env = Env()
simple validator
env.int(“TTL”, validate=lambda n: n > 0)
=> Environment variable “TTL” invalid: [‘Invalid value.’]
using marshmallow validators
env.str(
“NODE_ENV”,
validate=OneOf(
[“production”, “development”], error=“NODE_ENV must be one of: {choices}”
),
)
=> Environment variable “NODE_ENV” invalid: [‘NODE_ENV must be one of: production, development’]
multiple validators
env.str(“EMAIL”, validate=[Length(min=4), Email()])
=> Environment variable “EMAIL” invalid: [‘Shorter than minimum length 4.’, ‘Not a valid email address.’]
复制代码
在这里,我们通过 validate 方法,并传入一些判断条件。如 NODE_ENV 只允许传入 production 和 develpment 其中之一;EMAIL 必须符合 email 的格式。
这里依赖于 marshmallow 这个库,里面有很多验证条件,大家可以了解下。
如果不符合条件的,会直接抛错,例如:
marshmallow.exceptions.ValidationError: [‘Invalid value.’]
关于 marshmallow 库的用法,大家可以参考:https://marshmallow.readthedocs.io/en/stable/
最后再附一点我平时定义环境变量的一些常见写法,如:
复制代码
import platform
from os.path import dirname, abspath, join
from environs import Env
from loguru import logger
env = Env()
env.read_env()
definition of flags
IS_WINDOWS = platform.system().lower() == ‘windows’
definition of dirs
ROOT_DIR = dirname(dirname(abspath(file)))
LOG_DIR = join(ROOT_DIR, env.str(‘LOG_DIR’, ‘logs’))
definition of environments
DEV_MODE, TEST_MODE, PROD_MODE = ‘dev’, ‘test’, ‘prod’
APP_ENV = env.str(‘APP_ENV’, DEV_MODE).lower()
APP_DEBUG = env.bool(‘APP_DEBUG’, True if APP_ENV == DEV_MODE else False)
APP_DEV = IS_DEV = APP_ENV == DEV_MODE
APP_PROD = IS_PROD = APP_DEV == PROD_MODE
APP_TEST = IS_TEST = APP_ENV = TEST_MODE
redis host
REDIS_HOST = env.str(‘REDIS_HOST’, ‘127.0.0.1’)
redis port
REDIS_PORT = env.int(‘REDIS_PORT’, 6379)
redis password, if no password, set it to None
REDIS_PASSWORD = env.str(‘REDIS_PASSWORD’, None)
redis connection string, like redis://[password]@host:port or rediss://[password]@host:port
REDIS_CONNECTION_STRING = env.str(‘REDIS_CONNECTION_STRING’, None)
definition of api
API_HOST = env.str(‘API_HOST’, ‘0.0.0.0’)
API_PORT = env.int(‘API_PORT’, 5555)
API_THREADED = env.bool(‘API_THREADED’, True)
definition of flags
ENABLE_TESTER = env.bool(‘ENABLE_TESTER’, True)
ENABLE_GETTER = env.bool(‘ENABLE_GETTER’, True)
ENABLE_SERVER = env.bool(‘ENABLE_SERVER’, True)
logger
logger.add(env.str(‘LOG_RUNTIME_FILE’, ‘runtime.log’), level=‘DEBUG’, rotation=‘1 week’, retention=‘20 days’)
logger.add(env.str(‘LOG_ERROR_FILE’, ‘error.log’), level=‘ERROR’, rotation=‘1 week’)