Store config in the environment
Store config in the environment
https://12factor.net/config
The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.
pydantic_settings
https://fastapi.tiangolo.com/advanced/settings/#reading-a-env-file
fastapi推荐
https://docs.pydantic.dev/latest/concepts/pydantic_settings/#usage
from typing import Any, Callable, Set from pydantic import ( AliasChoices, AmqpDsn, BaseModel, Field, ImportString, PostgresDsn, RedisDsn, ) from pydantic_settings import BaseSettings, SettingsConfigDict class SubModel(BaseModel): foo: str = 'bar' apple: int = 1 class Settings(BaseSettings): auth_key: str = Field(validation_alias='my_auth_key') api_key: str = Field(alias='my_api_key') redis_dsn: RedisDsn = Field( 'redis://user:pass@localhost:6379/1', validation_alias=AliasChoices('service_redis_dsn', 'redis_url'), (3) ) pg_dsn: PostgresDsn = 'postgres://user:pass@localhost:5432/foobar' amqp_dsn: AmqpDsn = 'amqp://user:pass@localhost:5672/' special_function: ImportString[Callable[[Any], Any]] = 'math.cos' (4) # to override domains: # export my_prefix_domains='["foo.com", "bar.com"]' domains: Set[str] = set() # to override more_settings: # export my_prefix_more_settings='{"foo": "x", "apple": 1}' more_settings: SubModel = SubModel() model_config = SettingsConfigDict(env_prefix='my_prefix_') (5) print(Settings().model_dump()) """ { 'auth_key': 'xxx', 'api_key': 'xxx', 'redis_dsn': Url('redis://user:pass@localhost:6379/1'), 'pg_dsn': MultiHostUrl('postgres://user:pass@localhost:5432/foobar'), 'amqp_dsn': Url('amqp://user:pass@localhost:5672/'), 'special_function': math.cos, 'domains': set(), 'more_settings': {'foo': 'bar', 'apple': 1}, } """
使用.env覆盖默认值, 但是最终环境会覆盖.env值
from pydantic_settings import BaseSettings, SettingsConfigDict ''' default -> env file -> env variable ''' class Settings(BaseSettings): PATH: str = "Awesome API" USER: str = "xxx" TEST: str = "yyy" model_config = SettingsConfigDict(env_file=".env") if __name__ == "__main__": src.machine_learning_workflow_pipeline() print("---------------------------") print(Settings().PATH) print(Settings().USER) print(Settings().TEST)
https://docs.pydantic.org.cn/latest/concepts/pydantic_settings/#parsing-environment-variable-values
例如,给定以下环境变量
# your environment export V0=0 export SUB_MODEL='{"v1": "json-1", "v2": "json-2"}' export SUB_MODEL__V2=nested-2 export SUB_MODEL__V3=3 export SUB_MODEL__DEEP__V4=v4
您可以将它们加载到以下设置模型中
from pydantic import BaseModel from pydantic_settings import BaseSettings, SettingsConfigDict class DeepSubModel(BaseModel):v4: str class SubModel(BaseModel):v1: str v2: bytes v3: int deep: DeepSubModel class Settings(BaseSettings): model_config = SettingsConfigDict(env_nested_delimiter='__') v0: str sub_model: SubModel print(Settings().model_dump()) """ { 'v0': '0', 'sub_model': {'v1': 'json-1', 'v2': b'nested-2', 'v3': 3, 'deep': {'v4': 'v4'}}, } """
fastapi项目模版
https://github.com/fastapi/full-stack-fastapi-template/blob/master/backend/app/core/config.py
import secrets import warnings from typing import Annotated, Any, Literal from pydantic import ( AnyUrl, BeforeValidator, HttpUrl, PostgresDsn, computed_field, model_validator, ) from pydantic_core import MultiHostUrl from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Self def parse_cors(v: Any) -> list[str] | str: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] elif isinstance(v, list | str): return v raise ValueError(v) class Settings(BaseSettings): model_config = SettingsConfigDict( # Use top level .env file (one level above ./backend/) env_file="../.env", env_ignore_empty=True, extra="ignore", ) API_V1_STR: str = "/api/v1" SECRET_KEY: str = secrets.token_urlsafe(32) # 60 minutes * 24 hours * 8 days = 8 days ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 FRONTEND_HOST: str = "http://localhost:5173" ENVIRONMENT: Literal["local", "staging", "production"] = "local" BACKEND_CORS_ORIGINS: Annotated[ list[AnyUrl] | str, BeforeValidator(parse_cors) ] = [] @computed_field # type: ignore[prop-decorator] @property def all_cors_origins(self) -> list[str]: return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ self.FRONTEND_HOST ] PROJECT_NAME: str SENTRY_DSN: HttpUrl | None = None POSTGRES_SERVER: str POSTGRES_PORT: int = 5432 POSTGRES_USER: str POSTGRES_PASSWORD: str = "" POSTGRES_DB: str = "" @computed_field # type: ignore[prop-decorator] @property def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: return MultiHostUrl.build( scheme="postgresql+psycopg", username=self.POSTGRES_USER, password=self.POSTGRES_PASSWORD, host=self.POSTGRES_SERVER, port=self.POSTGRES_PORT, path=self.POSTGRES_DB, ) SMTP_TLS: bool = True SMTP_SSL: bool = False SMTP_PORT: int = 587 SMTP_HOST: str | None = None SMTP_USER: str | None = None SMTP_PASSWORD: str | None = None # TODO: update type to EmailStr when sqlmodel supports it EMAILS_FROM_EMAIL: str | None = None EMAILS_FROM_NAME: str | None = None @model_validator(mode="after") def _set_default_emails_from(self) -> Self: if not self.EMAILS_FROM_NAME: self.EMAILS_FROM_NAME = self.PROJECT_NAME return self EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48 @computed_field # type: ignore[prop-decorator] @property def emails_enabled(self) -> bool: return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL) # TODO: update type to EmailStr when sqlmodel supports it EMAIL_TEST_USER: str = "test@example.com" # TODO: update type to EmailStr when sqlmodel supports it FIRST_SUPERUSER: str FIRST_SUPERUSER_PASSWORD: str def _check_default_secret(self, var_name: str, value: str | None) -> None: if value == "changethis": message = ( f'The value of {var_name} is "changethis", ' "for security, please change it, at least for deployments." ) if self.ENVIRONMENT == "local": warnings.warn(message, stacklevel=1) else: raise ValueError(message) @model_validator(mode="after") def _enforce_non_default_secrets(self) -> Self: self._check_default_secret("SECRET_KEY", self.SECRET_KEY) self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD) self._check_default_secret( "FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD ) return self settings = Settings() # type: ignore
dynaconf
https://www.dynaconf.com/#reading-settings-from-files
Features¶
- Inspired by the 12-factor application guide
- Settings management (default values, validation, parsing, templating)
- Protection of sensitive information (passwords/tokens)
- Multiple file formats
toml|yaml|json|ini|pyand also customizable loaders.- Full support for environment variables to override existing settings (dotenv support included).
- Optional layered system for multi environments
[default, development, testing, production](also called multi profiles)- Built-in support for Hashicorp Vault and Redis as settings and secrets storage.
- Built-in extensions for Django and Flask web frameworks.
- CLI for common operations such as
init, list, write, validate, export.

浙公网安备 33010602011771号