Apsi
Browse the docs · Apsi 2.4.4

Docs 2.4.4 · 3 min read

Configuration, secrets and virtual environments

.env files (use env)

use env
env.load(".env")                         # KEY=value lines
let db_url = env.get("DATABASE_URL")     # the .env file first, then the real environment
let key = env.secret("OPENAI_API_KEY")   # prints as ******; key.reveal() gives the text
let port = env.int("PORT", 8080)
let debug = env.bool("DEBUG", false)     # true/false, 1/0, yes/no, on/off
let hosts = env.list("ALLOWED_HOSTS")    # comma-separated
env.require("DATABASE_URL", "SECRET_KEY")   # one clear error listing everything that is missing

The file format is the one python-dotenv and Docker Compose read: KEY=value, export KEY=value, single and double quotes, multi-line double-quoted values, \n escapes in double quotes, # comments, and ${OTHER} expansion.

apsi run app.apsi --env-file .env loads the file before the program starts.

Permissions: values from a .env file need only --allow-read for that file. Reading the process environment needs --allow-env (or --allow-env=PORT,DEBUG for named variables). A default (env.int("PORT", 8080)) is used when the permission is missing, so a program still starts.

Secrets stay secret: a Secret prints and logs as ******, is left out of error messages, and compares in constant time. Pass it to db.postgres(...), web clients or crypto functions directly; call .reveal() only where the text itself is needed.

Python virtual environments

apsi run app.apsi --venv .venv          # pyimport() uses the packages installed in that venv
apsi run app.apsi                        # a .venv or venv folder next to the program is found automatically

The venv's site-packages are put first on the import path for pyimport/pyfrom and converted programs. The venv's Python version must match the one Apsi runs on (Apsi says so when it does not).

Reference (use env)

Function Returns Behaviour
env.load(path=".env", override=true, export=false) names defined Reads a .env file. A missing file is an error. With override=false, names already loaded keep their value. With export=true, the values are also put into the process environment for child processes. Needs --allow-read for the file.
env.get(name, default=none) Str or default The loaded .env value first, then the process environment. Without a permission to read the environment, returns default when one is given and raises a permission error otherwise.
env.has(name) Bool Whether the name is set in the loaded files or the environment.
env.require(name, …) the values Raises one error listing every missing or empty name, so a deployment shows all problems at once.
env.secret(name, default=none) Secret or none The value wrapped so it never prints. .reveal() returns the text.
env.int(name, default=none), env.float(…) number Converts the value. An empty or missing value gives default. A value that is not a number is an error (never silently 0).
env.bool(name, default=false) Bool 1, true, yes, on, y (any case) are true; any other set value is false.
env.list(name, sep=",", default=none) List[Str] Splits and trims; empty items are dropped.
env.parse(text) Dict Parses .env text without touching any state — useful in tests.
env.values Dict Everything loaded from files so far.

File format in detail

# comments start with #
PORT=8080
export DEBUG=true                     # "export " is allowed and ignored
NAME='single quotes: taken literally, no ${EXPANSION}'
GREETING="double quotes: \n becomes a line break"
KEY="-----BEGIN KEY-----
spans several lines
-----END KEY-----"
DATABASE_URL=postgres://${DB_USER}:${DB_PASS}@db/app   # ${NAME} from this file or the environment
EMPTY=
TRAILING=value   # an unquoted value ends before " #"

A byte-order mark at the start of the file (common with Windows editors) is ignored.

Secrets in practice

use env
use db
env.load()
let pg = db.postgres(host=env.get("DB_HOST"), user="app", password=env.secret("DB_PASSWORD"))
print(env.secret("DB_PASSWORD"))          # ******
log(f"connecting with {env.secret("DB_PASSWORD")}")   # connecting with ******
  • Keep .env out of version control (add it to .gitignore) and commit a .env.example with the names and harmless values instead.
  • On servers, prefer real environment variables or your platform's secret store, and run with --allow-env=NAME1,NAME2 so the program can read only what it needs.
  • Secret values compare in constant time, so checking an API key with == does not leak it through timing.

Found a mistake? Report it · Markdown source