下面推荐一个 environs 库,利用它我们可以轻松地设置各种类型的环境变量。
安装:
pip3 install environs
好,安装之后,我们再来体验一下使用 environs 来设置环境变量的方式。
from environs import Envenv = Env()
VAR1 = env.int('VAR1', 1)
VAR2 = env.float('VAR2', 5.5)
VAR3 = env.list('VAR3')
这里 environs 直接提供了 int、float、list 等方法,我们就不用再去进行类型转换了。
与此同时,设置环境变量的方式也有所变化:
export VAR1=1export VAR2=2.3
export VAR3=1,2
这里 VAR3 是列表,我们可以直接用逗号分隔开来。
打印结果如下:
12.3
['1', '2']
下面我们再看一个官方示例,这里示例了一些常见的用法。
首先我们来定义一些环境变量,如下:
export GITHUB_USER=sloriaexport 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 Envenv = 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 optionalsubcast keyword argument)
- env.dict (accepts optionalsubcast 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 apathlib.Path)
这里 list、dict、json、date、url、uuid、path 个人认为都还是比较有用的,另外 list、dict 方法还有一个 subcast 方法可以对元素内容进行转化。
对于 dict、url、date、uuid、path 这里我们来补充说明一下。
下面我们定义这些类型的环境变量:
export VAR_DICT=name=germey,age=25export 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 Envenv = 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)
运行结果:
{'name': 'germey', 'age': '25'}
{'name': 'germey', 'age': 25}
ParseResult(scheme='https', netloc='cuiqingcai.com', path='', params='', query='', fragment='')
762c8d53-5860-4d5d-81bc-210bf2663d0e
/var/py/env
可以看到,它分别给我们转化成了 dict、dict、ParseResult、UUID、PosixPath 类型了。
在代码中直接使用即可。
文件读取
如果我们的一些环境变量是定义在文件中的,environs 还可以进行读取和加载,默认会读取本地当前运行目录下的 .env 文件。
示例如下:
from environs import Envenv = 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=falseAPP_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 platformfrom 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')