下面推荐一个 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 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=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')