Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Showcase: yaml config management with secret env var placeholders #672

Copy link
Copy link
@ion-elgreco

Description

@ion-elgreco
Issue body actions

There isn't a discussion board, so I thought to share it in an issue since it's a pretty nice :). With the setup below, you can actually create yamls with this structure with env var placeholders using pydantic basesettings:

mcp_servers:
  - endpoint: "https://hello_world"
    api_key: ${EXAMPLE_KEY}

And the ${} are environment variables evaluated at runtime, when initializing the config.

You have to create a custom YamlConfigSettingSource, that does that:

import os
from functools import lru_cache
from pathlib import Path
from string import Template
from typing import Any

from pydantic import BaseModel, Field, SecretStr
from pydantic_settings import (
    BaseSettings,
    PydanticBaseSettingsSource,
    SettingsConfigDict,
    YamlConfigSettingsSource,
)

class EnvParsedYamlConfigSettingsSource(YamlConfigSettingsSource):
    """Yaml Settings Source that parses envVar references before loading.

    ENV VARS need to be configured like this: ${ENV_VAR}

    An example of a valid config.yaml would be:
        ```yaml
        config:
            api_key: ${OPENAI_KEY}
        ```
    """

    def _read_file(self, file_path: Path) -> dict[str, Any]:
        import json

        data = super()._read_file(file_path)
        data = json.dumps(data)
        data = Template(data).substitute(os.environ)
        return json.loads(data)

Now we can proceed with creating an example Config:

class MCPServer(BaseModel):
    """MCP Server connection"""

    endpoint: str
    api_key: SecretStr

class MCPServerConfiguration(BaseSettings):
    """MCP server configuration"""

    servers: list[MCPServer] | None = Field(alias="mcp_servers", default=None)
    model_config = SettingsConfigDict(
        extra="ignore",
        yaml_file=os.environ.get(
            "CONFIG_FILE_PATH",
            Path(__file__).parent.parent / "config.yaml",
        ),
    )

    @classmethod
    def settings_customise_sources(  # noqa: D102
        cls,
        settings_cls: type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,  # noqa: ARG003
        env_settings: PydanticBaseSettingsSource,  # noqa: ARG003
        dotenv_settings: PydanticBaseSettingsSource,  # noqa: ARG003
        file_secret_settings: PydanticBaseSettingsSource,  # noqa: ARG003
    ) -> tuple[PydanticBaseSettingsSource, ...]:
        return (EnvParsedYamlConfigSettingsSource(settings_cls),)


class Config(BaseSettings):
    """Application settings"""

    mcp: MCPServerConfiguration = MCPServerConfiguration()  # type: ignore

    # API Configuration
    api_host: str = Field(default="0.0.0.0", description="API host")
    api_port: int = Field(default=8000, description="API port")

    class Config:
        """Config class for .env values"""

        env_file = ".env"
        env_file_encoding = "utf-8"
        case_sensitive = True
        extra = "ignore"


@lru_cache(1)
def get_config() -> Config:
    """Lazy static to get the config for the project"""
    config = Config()  # type: ignore[call-arg]`
    return config  # type: ignore

So let's bring this together and run:

print(config)
print(config.mcp.servers[0].api_key.get_secret_value())

export EXAMPLE_KEY='12345678' && python test.py, which will print out the following:

mcp=MCPServerConfiguration(servers=[MCPServer(endpoint='https://hello_world', api_key=SecretStr('**********'))]) api_host='0.0.0.0' api_port=8000
12345678

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions

    Morty Proxy This is a proxified and sanitized view of the page, visit original site.