環境変数で設定を管理したくなった場合のpython-dotenvのメモ
最近は設定ファイルだけではなく環境変数で設定を渡すことがけっこうある(コンテナ的な文脈だったり、12 factor app的な文脈だったり色々)。自前で頑張っても良いけれど。自分でコードを書かずに済ませたい。
何か良いライブラリは無いかなと探してみたらあったので使いかたのメモ。
python-dotenv
Get and set values in your .env file in local and production servers. tada
install
$ pip install python-dotenv
挙動
getting startedを読むだけでは挙動を把握できなかったので色々調べてみた。
挙動を一言で言うと、os.environに対してdefault値を与えるもの。
たとえば以下のような.env
があるとする。
.env
prefix=hello message=${prefix} world
このとき環境変数を指定して実行せずともdefault値として.envに記述された値が使われる。 (正確に言うなら環境変数として指定された名前の値を保持していない場合に.envから読み込む)
main.py
import os from dotenv import load_dotenv load_dotenv(verbose=True) print("prefix", os.environ.get("prefix")) print("message", os.environ.get("message"))
ここで
defaultは.envに記述された値
$ python main.py prefix hello message hello world
環境変数を与えるとその値が使われる
$ prefix=bye python main.py prefix bye message bye world
case sensitive(大文字小文字を区別する)
$ PREFIX=bye python main.py prefix hello message hello world
なのでlocal用のaccess tokenを.envに記述し本番では環境変数越しに値を渡すということが想定された使われ方(12 factor app的な文脈)。
override=True
override=Trueを使うとdotenv側の設定を尊重する。正直使いみちはあまりないと思う。
$ python main2.py prefix hello message hello world $ prefix=bye python main2.py prefix hello message hello world
環境変数で指定した値が使われなくなる。
変更のdiff
--- main.py 2019-04-29 15:48:35.499386628 +0900 +++ main2.py 2019-04-29 16:29:58.014889108 +0900 @@ -1,6 +1,6 @@ import os from dotenv import load_dotenv -load_dotenv(verbose=True) +load_dotenv(verbose=True, override=True) print("prefix", os.environ.get("prefix")) print("message", os.environ.get("message"))
.envで設定した部分だけを取り出す
これは dotenv_values()
で取り出せる。configとして扱う分にはこちらのほうが便利かもしれない。
from dotenv import load_dotenv, dotenv_values load_dotenv(verbose=True) print(dotenv_values(verbose=True))
実行結果
{'prefix': 'hello', 'message': 'hello world'}
このときの.env
prefix=hello message=${prefix} world
追記
:warning: 環境変数込みで渡した場合にも.env側の値が取り出されるので、configとして使うには危険かも(実装を考えるとこの挙動はあり得る)
$ python main.py {'prefix': 'hello', 'message': 'hello world'} $ prefix=bye python main*.py {'prefix': 'hello', 'message': 'bye world'}
prefixは.envに書かれたものが使われる。一方messageは環境変数の解釈が入りhelloではなくbyeが使われる。
ちょっとひと手間だけれど、以下の様にすれば期待したふるまいになりそう。
import os print({k: os.environ.get(k) for k in dotenv_values()}) # {'prefix': 'bye', 'message': 'bye world'}
複数の.envが用意されていた場合に読まれる.envについて
これはfind_dotenv()
の実行で確認できる。複数のファイルを全部マージするなどと言ったマジカルな挙動は存在しない。最初に見つかった.envファイルを使う。それでおしまい。
ただし現在の位置に存在しなかった場合に親方向を辿って探してはくれる。
$ tree -a . |-- .env |-- main.py |-- sub | |-- .env | `-- main.py `-- sub2 `-- main.py 2 directories, 5 files
ここでそれぞれ見られる.envは以下の様な形。
- main.py は.env
- sub/main.py は sub/.env
- sub2/main.py は .env
調査用のコードは以下のようなもの。
main.py
from dotenv import find_dotenv, dotenv_values print(find_dotenv()) print(dotenv_values())
実行結果
$ python main.py ~/my/example_dotenv/03find/.env {'x': 'root'} $ python sub/main.py ~/my/example_dotenv/03find/sub/.env {'x': 'sub', 'y': 'value'} $ python sub2/main.py ~/my/example_dotenv/03find/.env {'x': 'root'}
このときの.env
.env
x="root"
sub/.env
x=sub y=value
subprocessの実行時の環境変数
subprocessに環境変数を引き継ぐ方法は通常のsubprocessの利用と変わらない。dotenvで読み込んだときに環境変数に設定するので。むしろこれのために環境変数ベースで設定をしていると言っても良い(processとsubprocessとの間でのdynamic scope的な辞書)。
たとえば先程の.env(再掲)を以下の様なsubprocessを利用したコードで使う。
.env
prefix=hello message=${prefix} world
subprocessを利用したコード
main2.py
import subprocess from dotenv import load_dotenv load_dotenv(verbose=True) cmd = ["python", "-c", "import os; print(os.environ.get('message', '<none>'))"] subprocess.run(cmd, check=True)
messageという環境変数が引き継がれる。
$ python main2.py hello world
個別に設定したい場合
これはオフトピックだけれど、個別に設定したい場合はsubprocess.run
のenv
オプションに辞書を渡せば良い。
main3.py
import os import subprocess from dotenv import load_dotenv load_dotenv(verbose=True) cmd = ["python", "-c", "import os; print(os.environ.get('message', '<none>'))"] myenv = os.environ.copy() myenv["message"] = "bye, bye" subprocess.run(cmd, check=True) subprocess.run(cmd, check=True, env=myenv) subprocess.run(cmd, check=True)
結果
$ python main3.py hello world bye, bye hello world
jsonschemaやopenAPI docのファイルを分解する機能を作った
背景
jsonschemaやopenAPI documentを触っている時に、時折、例としてあげられるファイルが巨大でインデントなどの対応を把握するのが辛くなることがある。
自分で書く場合には複数のファイルに分解された状態で書くので問題ないのだけれど。
手動で分解するのも面倒だったので。分解する機能をjsonknifeに追加した。
機能
separateという名前にした。適切な名前な気がしないのでもう少し良い名前を付けたい。 separateは過去に作ったbundleの機能と対になっている。
以下の様な対応関係。
- bundle -- 複数のファイルをまとめて1つのファイルにする
- separate -- 1つのファイルを分解して複数のファイルにする
例
例えば以下のようなJSONSchemaの定義があるとする。"#/definitions/address" の部分だけでも別ファイルになれば見やすいのになーと思ったりしたとする。
schema.json
{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "address": { "properties": { "city": { "$ref": "#/definitions/name" }, "state": { "type": "string" }, "street_address": { "type": "string" } }, "required": [ "street_address", "city", "state" ], "type": "object" }, "name": { "type": "string" } }, "properties": { "billing_address": { "$ref": "#/definitions/address" }, "shipping_address": { "$ref": "#/definitions/address" } }, "type": "object" }
このときにseparateを使って1つのファイル(schema.json)を複数のファイルに分解する。
$ jsonknife separate --src schema.json --format json --dst main.json INFO:jsonknife.separator:emit file definitions/address.json INFO:jsonknife.separator:emit file definitions/name.json INFO:jsonknife.separator:emit file main.json
3つのファイルに分解された。
. ├── main.json ├── definitions │ ├── address.json │ └── name.json └── schema.json # これは元のファイル
分解された個々のファイルは以下のような内容になっている。
main.json
{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "address": { "$ref": "definitions/address.json#/definitions/address" } }, "properties": { "billing_address": { "$ref": "#/definitions/address" }, "shipping_address": { "$ref": "#/definitions/address" } }, "type": "object" }
main.jsonがentrypoint的なもの。参照先は別のファイルになっているので全体の構造が把握しやすい。
(トップレベルのdefinitionsを削除してしまっても良い気もしたが残している。)
次に参照先のaddressの定義。ファイルの位置関係は元のファイルの改装構造とディレクトリの位置を対応させている。
definitions/address.json
{ "definitions": { "address": { "properties": { "city": { "$ref": "name.json#/definitions/name" }, "state": { "type": "string" }, "street_address": { "type": "string" } }, "required": [ "street_address", "city", "state" ], "type": "object" } } }
addressからしかnameは参照されないのでmain.jsonのdefinitionsにはnameが現れていない。
そのnameは以下のようなもの。
definitions/name.json
{ "definitions": { "name": { "type": "string" } } }
再帰的定義
再規定機な定義にも対応している。
例えば以下のようなファイル。
schema2.json
{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "person": { "properties": { "children": { "default": [], "items": { "$ref": "#/definitions/person" }, "type": "array" }, "name": { "type": "string" } }, "type": "object" } }, "properties": { "person": { "$ref": "#/definitions/person" } }, "type": "object" }
これは以下のように(defaultはyamlで出力)
. ├── main2.yaml ├── definitions │ └── person.yaml └── schema2.json # 元のファイル
main2.yaml
$schema: 'http://json-schema.org/draft-07/schema#' definitions: person: $ref: 'definitions/person.yaml#/definitions/person' properties: person: $ref: '#/definitions/person' type: object
definitions/person.yaml
definitions: person: properties: children: default: [] items: $ref: '#/definitions/person' type: array name: type: string type: object
複雑なもの
複雑なファイルに実行するとけっこうすごい感じになる。 たとえば speccyが持っているopenAPI3.0のjsonschemaを渡して分解したときに作られるファイルは以下の様なもの。
すごくたくさんのファイルが作られる。
. ├── main.json ├── definitions │ ├── APIKeySecurityScheme.json │ ├── AuthorizationCodeOAuthFlow.json │ ├── BearerHTTPSecurityScheme.json │ ├── Callback.json │ ├── ClientCredentialsFlow.json │ ├── Components.json │ ├── Contact.json │ ├── Discriminator.json │ ├── Encoding.json │ ├── Example.json │ ├── ExternalDocumentation.json │ ├── HTTPSecurityScheme.json │ ├── Header.json │ ├── HeaderWithContent.json │ ├── HeaderWithSchema.json │ ├── HeaderWithSchemaWithExample.json │ ├── HeaderWithSchemaWithExamples.json │ ├── ImplicitOAuthFlow.json │ ├── Info.json │ ├── License.json │ ├── Link.json │ ├── LinkWithOperationId.json │ ├── LinkWithOperationRef.json │ ├── MediaType.json │ ├── MediaTypeWithExample.json │ ├── MediaTypeWithExamples.json │ ├── NonBearerHTTPSecurityScheme.json │ ├── OAuth2SecurityScheme.json │ ├── OAuthFlows.json │ ├── OpenIdConnectSecurityScheme.json │ ├── Operation.json │ ├── Parameter.json │ ├── ParameterWithContent.json │ ├── ParameterWithContentInPath.json │ ├── ParameterWithContentNotInPath.json │ ├── ParameterWithSchema.json │ ├── ParameterWithSchemaWithExample.json │ ├── ParameterWithSchemaWithExampleInCookie.json │ ├── ParameterWithSchemaWithExampleInHeader.json │ ├── ParameterWithSchemaWithExampleInPath.json │ ├── ParameterWithSchemaWithExampleInQuery.json │ ├── ParameterWithSchemaWithExamples.json │ ├── ParameterWithSchemaWithExamplesInCookie.json │ ├── ParameterWithSchemaWithExamplesInHeader.json │ ├── ParameterWithSchemaWithExamplesInPath.json │ ├── ParameterWithSchemaWithExamplesInQuery.json │ ├── PasswordOAuthFlow.json │ ├── PathItem.json │ ├── Paths.json │ ├── Reference.json │ ├── RequestBody.json │ ├── Response.json │ ├── Responses.json │ ├── Schema.json │ ├── SecurityRequirement.json │ ├── SecurityScheme.json │ ├── Server.json │ ├── ServerVariable.json │ ├── Tag.json │ └── XML.json └── schema.json # 元のファイル
元のファイル(schema.json)と変換後のファイル(main.json)行数の差異。
$ wc schema.json main.json 2346 5165 80453 schema.json 71 145 1761 main.json 2417 5310 82214 total
71行くらいなら全体を把握できる。
main.json
{ "type": "object", "required": [ "openapi", "info", "paths" ], "properties": { "openapi": { "type": "string", "pattern": "^3\\.0\\.\\d(-.+)?$" }, "info": { "$ref": "#/definitions/Info" }, "externalDocs": { "$ref": "#/definitions/ExternalDocumentation" }, "servers": { "type": "array", "items": { "$ref": "#/definitions/Server" } }, "security": { "type": "array", "items": { "$ref": "#/definitions/SecurityRequirement" } }, "tags": { "type": "array", "items": { "$ref": "#/definitions/Tag" } }, "paths": { "$ref": "#/definitions/Paths" }, "components": { "$ref": "#/definitions/Components" } }, "patternProperties": { "^x-": {} }, "additionalProperties": false, "definitions": { "Info": { "$ref": "definitions/Info.json#/definitions/Info" }, "Server": { "$ref": "definitions/Server.json#/definitions/Server" }, "Components": { "$ref": "definitions/Components.json#/definitions/Components" }, "Paths": { "$ref": "definitions/Paths.json#/definitions/Paths" }, "SecurityRequirement": { "$ref": "definitions/SecurityRequirement.json#/definitions/SecurityRequirement" }, "Tag": { "$ref": "definitions/Tag.json#/definitions/Tag" }, "ExternalDocumentation": { "$ref": "definitions/ExternalDocumentation.json#/definitions/ExternalDocumentation" } }, "description": "This is the root document object for the API specification. It combines what previously was the Resource Listing and API Declaration (version 1.2 and earlier) together into one document." }
テストは?
テストは実際に実行した結果のdiffを見てる。簡易的な回帰テスト。 (CIでもexamplesを全部実行した後にgit diffでdiffがなければOKというもの)
一応、元のファイルとseparate後bundleしたファイルが同じ内容なことを確認している。
https://github.com/podhmo/dictknife/blob/master/examples/jsonknife/separate/Makefile