Skip to main content

Environment-aware configuration loader with GCP Secret Manager support

Project description

env-manager

A simple, environment-aware configuration manager that unifies secrets from local .env files and Google Cloud Secret Manager. It handles type coercion, validation, secret masking, and automatically loads variables to os.environ so external libraries work seamlessly.

Installation

Add to your pyproject.toml:

[project]
dependencies = [
    "env-manager @ git+https://github.com/NotoriosTI/env-manager.git@main",
]

Then install with Poetry or pip.

Quickstart

Basic Usage (Recommended)

The singleton pattern is the simplest way to use env-manager:

# main.py - Initialize once at startup
from env_manager import init_config, get_config

init_config("config/config_vars.yaml")

# Now use anywhere in your codebase
db_password = get_config("DB_PASSWORD")
api_timeout = get_config("API_TIMEOUT", 30)  # with default

What happens automatically:

  • Variables are loaded from .env or GCP Secret Manager
  • Type coercion is applied (strings → int/float/bool as configured)
  • Values are validated (required vs optional)
  • Everything is assigned to os.environ
  • External libraries (LangChain, LangGraph, etc.) work automatically

Example:

# config_vars.yaml defines PORT as type: int
init_config("config/config_vars.yaml")

port = get_config("PORT")  # → 8080 (actual int, not string)
# os.environ["PORT"] is now "8080" (string, for external libraries)

Secret Sources

By default, secrets are loaded from .env files. To use Google Cloud Secret Manager, set SECRET_ORIGIN=gcp.

Priority order:

  1. Explicit parameter: init_config(..., secret_origin="gcp")
  2. Environment variable: export SECRET_ORIGIN=gcp
  3. .env file: SECRET_ORIGIN=gcp
  4. Default: "local"

Configuration File

Create a YAML file (e.g., config_vars.yaml) with this structure:

# Optional: define named environments (selected via ENVIRONMENT env var)
environments:
  production:
    origin: gcp
    gcp_project_id: my-gcp-project
  local:
    origin: local
    dotenv_path: .env
    default: true   # Used when ENVIRONMENT is not set

variables:
  # Required secret (must exist in .env or GCP)
  DB_PASSWORD:
    source: DB_PASSWORD
    type: str

  # Optional with fallback
  PORT:
    source: PORT
    type: int
    default: 8080

  # Constant (no external source needed)
  LOG_LEVEL:
    type: str
    default: "INFO"

  # Per-variable origin override
  ANALYTICS_KEY:
    source: ANALYTICS_KEY
    type: str
    origin: gcp                    # Always fetch from GCP, regardless of global setting
    dotenv_path: secrets/.env.gcp  # Optional: custom .env path for this variable

validation:
  strict: false
  required:
    - DB_PASSWORD    # Error if missing
  optional:
    - DEBUG_MODE     # Warning if missing

Variable Definition Rules

Each variable must have at least one of:

  • source: Name of the secret in .env or GCP Secret Manager
  • default: Fallback value if not found

Optional per-variable overrides:

  • origin: "local" or "gcp" — overrides the global secret_origin for this variable
  • environment: Name of a defined environment to use as the source context for this variable
  • dotenv_path: Path to a custom .env file for this variable (relative to project root)

Type coercion (type field):

  • str (default): No conversion
  • int: Converts to integer
  • float: Converts to float
  • bool: Accepts only "true", "True", "1", "false", "False", "0"

Validation (validation section):

  • required: Raises error if variable is missing
  • optional: Logs warning if variable is missing
  • strict: true: Enforces all variables must have values (ignores defaults)

Environments (environments section):

  • Named environments with their own origin, dotenv_path, and/or gcp_project_id
  • Active environment selected via ENVIRONMENT env var
  • Mark one as default: true to use when ENVIRONMENT is not set

Complete API Reference

Singleton API (Recommended)

from env_manager import init_config, get_config, require_config

# Initialize once
init_config(
    "config/config_vars.yaml",
    secret_origin=None,      # "local" or "gcp" (auto-detected if None)
    gcp_project_id=None,     # Required if secret_origin="gcp"
    strict=None,             # Override YAML strict setting
    dotenv_path=None,        # Custom .env path (auto-detected if None)
    debug=False,             # Show raw secrets in logs (NEVER in production)
)

# Use anywhere
value = get_config("KEY")                  # Returns value or None
value = get_config("KEY", "default")       # Returns value or provided default
value = require_config("REQUIRED_KEY")     # Raises RuntimeError if missing

Instance API (Advanced)

When to use ConfigManager directly:

  • Multiple configurations simultaneously
  • Advanced testing with dependency injection
  • Complex microservices architectures
from env_manager import ConfigManager

manager = ConfigManager(
    config_path="config/config_vars.yaml",
    secret_origin=None,      # "local" or "gcp" (falls back to env var)
    gcp_project_id=None,     # overrides discovery from env/.env
    strict=None,             # overrides YAML strict flag
    auto_load=True,          # eagerly load and validate
    debug=False,             # set True to log raw secret values
)

manager.get("DB_PASSWORD")        # Returns value or None
manager.get("PORT", 8080)         # Returns value or provided default
manager.require("API_KEY")        # Raises RuntimeError if missing
manager.values                    # Dict of all loaded values

Low-level Loader API (Expert)

For custom integrations, the loader abstraction is also exported:

from env_manager import SecretLoader, create_loader

# Create a loader directly
loader = create_loader("local", dotenv_path=".env")          # DotEnvLoader
loader = create_loader("gcp", gcp_project_id="my-project")  # GCPSecretLoader

# Fetch secrets
values = loader.get_many(["DB_PASSWORD", "API_KEY"])
# → {"DB_PASSWORD": "secret", "API_KEY": "key123"}

create_loader raises ValueError for unsupported origins (anything other than "local" or "gcp").

Example: Multiple configurations

prod_config = ConfigManager("config/prod.yaml", secret_origin="gcp")
dev_config = ConfigManager("config/dev.yaml", secret_origin="local")

prod_db = prod_config.get("DB_PASSWORD")
dev_db = dev_config.get("DB_PASSWORD")

Example: Testing with dependency injection

class DatabaseService:
    def __init__(self, config: ConfigManager = None):
        self.config = config or ConfigManager("config/config_vars.yaml")
        self.host = self.config.get("DB_HOST")

def test_database_service():
    test_config = ConfigManager("config/test.yaml")
    service = DatabaseService(config=test_config)
    # Test in isolation

How It Works

Automatic Environment Loading

When you call init_config() or create a ConfigManager:

  1. Configuration is parsed from YAML
  2. Secrets are fetched from .env or GCP Secret Manager
  3. Types are coerced according to YAML definitions
  4. Values are validated (required/optional checks)
  5. Variables are assigned to os.environ automatically
  6. Secrets are masked in all log output

This means external libraries that read from os.environ (like LangChain, LangGraph, etc.) work automatically without any additional setup.

Type Coercion Details

When you use get_config(): You get the properly typed value

port = get_config("PORT")  # → 8080 (int)
debug = get_config("DEBUG")  # → False (bool)

When external libraries read os.environ: They get strings

os.environ["PORT"]   # → "8080" (string)
os.environ["DEBUG"]  # → "false" (string)

This is intentional - os.environ only stores strings, but external libraries handle string parsing correctly.

SECRET_ORIGIN Resolution

The SECRET_ORIGIN determines where to load secrets from:

Priority (highest to lowest):

  1. Explicit parameter: init_config(..., secret_origin="gcp")
  2. Environment variable: export SECRET_ORIGIN=gcp
  3. .env file: SECRET_ORIGIN=gcp (read without loading entire file)
  4. Default: "local"

Example:

# .env file
SECRET_ORIGIN=gcp
GCP_PROJECT_ID=my-project
# Automatically uses GCP from .env
init_config("config/config_vars.yaml")

GCP_PROJECT_ID Resolution

When using secret_origin="gcp", the GCP project ID is resolved with:

  1. Explicit parameter: init_config(..., gcp_project_id="my-project")
  2. Environment variable: export GCP_PROJECT_ID=my-project
  3. .env file: GCP_PROJECT_ID=my-project
  4. Not set (warning logged)

Secret Masking

All secrets are automatically masked in logs for security:

  • Short secrets (< 10 chars): **********
  • Long secrets: ab****1234 (first 2 + last 4 chars shown)

Set debug=True to temporarily see raw values (never use in production):

init_config("config/config_vars.yaml", debug=True)

Migration Guide

If you're migrating from python-dotenv or manual os.environ usage:

  1. Copy config_vars.yaml.example to your project
  2. Customize the YAML with your variables
  3. Install env-manager in your dependencies
  4. Replace load_dotenv() with init_config("config/config_vars.yaml")
  5. Replace os.environ["KEY"] with get_config("KEY")
  6. Configure SECRET_ORIGIN=gcp and GCP_PROJECT_ID for production

Before:

from dotenv import load_dotenv
import os

load_dotenv()
db_password = os.environ["DB_PASSWORD"]
port = int(os.environ.get("PORT", "8080"))

After:

from env_manager import init_config, get_config

init_config("config/config_vars.yaml")
db_password = get_config("DB_PASSWORD")
port = get_config("PORT")  # Already an int, with default 8080

Troubleshooting

Configuration manager not initialised

  • Call init_config() before using get_config() or require_config()

Missing GCP project ID

  • Set GCP_PROJECT_ID via parameter, environment variable, or .env file

Type coercion failed

  • Check the YAML type field matches your value format
  • Ensure boolean values are exactly "true", "false", "1", or "0"

Required variable not found

  • Verify the secret exists in .env or GCP Secret Manager
  • Check the secret name matches the YAML source field
  • Ensure GCP credentials have access to the project

Variables not loaded

  • Confirm init_config() was called successfully
  • Check logs for warnings or errors
  • Verify .env file exists and is in the correct location

Development

# Install dependencies
poetry install

# Run tests
pytest -v

# Run with coverage
pytest --cov=env_manager --cov-report=html

The project uses Python 3.13+, Poetry for dependency management, and pytest for testing.

Related Projects

Looking for a TypeScript/Node.js version? Check out env-manager-js — a TypeScript implementation with full feature parity. Both projects share the same YAML config format, secret resolution logic, and API design, so you can use whichever fits your stack.

License

Copyright (c) 2025 NotoriosTI. All rights reserved.

This software is proprietary and confidential. Unauthorized copying, distribution, or use of this software, in whole or in part, is strictly prohibited.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

notoriosti_env_manager-0.2.1.tar.gz (20.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

notoriosti_env_manager-0.2.1-py3-none-any.whl (22.0 kB view details)

Uploaded Python 3

File details

Details for the file notoriosti_env_manager-0.2.1.tar.gz.

File metadata

  • Download URL: notoriosti_env_manager-0.2.1.tar.gz
  • Upload date:
  • Size: 20.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.3.2 CPython/3.14.3 Darwin/25.4.0

File hashes

Hashes for notoriosti_env_manager-0.2.1.tar.gz
Algorithm Hash digest
SHA256 641249d285b795d6af9cc1ace98ecc4fceaf3f0f602c66983e81042dfab32f2b
MD5 4650981c292452ab6fe715802ddee205
BLAKE2b-256 992074c47dea4130e7963526f74120d1135ad1ad35f821ea2fb2e23e0bdba001

See more details on using hashes here.

File details

Details for the file notoriosti_env_manager-0.2.1-py3-none-any.whl.

File metadata

File hashes

Hashes for notoriosti_env_manager-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f4858327522ef03d09cece0d81049aac62f56bb4f6cd3ccc7f4f20cea370f9e2
MD5 d23d5424a11b92623b91124d4db93fba
BLAKE2b-256 7bf9790a18d2973f599895044935daf7d6e6b745008f8105b77b46232ee4b004

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page