Skip to content

[EPIC-4-2] Configuration: Add YAML config loader with environment variable support - #27

Merged
deserat merged 1 commit into
developfrom
feature/EPIC-4-2-config-loader
Jan 25, 2026
Merged

[EPIC-4-2] Configuration: Add YAML config loader with environment variable support#27
deserat merged 1 commit into
developfrom
feature/EPIC-4-2-config-loader

Conversation

@deserat

@deserat deserat commented Jan 25, 2026

Copy link
Copy Markdown
Owner

Summary

Implements configuration loader for api-service that merges YAML files with environment variable overrides, providing a flexible configuration management system following 12-factor app methodology.

Related Issue

Closes #15 (EPIC-4-2: Configuration Loaders with Environment Variable Support)

Changes

New Configuration System

  • Config Class: Singleton pattern configuration loader with YAML + environment variable support
  • Priority Order: Environment variables > .env file > config.yaml > defaults
  • Validation: Fail-fast validation at startup with helpful error messages
  • Path Expansion: Support for ~ (tilde) and environment variables in file paths

Configuration Sections

  • database: PostgreSQL connection settings (url, pool_size, max_overflow, etc.)
  • vault: Vault path configuration (for development/testing)
  • scheduler: Background job settings (surfacing interval, batch size)
  • ai: AI/LLM configuration (model selection, API keys, temperature, tokens)
  • logging: Logging configuration (level, format, file path)
  • api: API server settings (host, port, debug mode, CORS origins)

Files Created

  • api-service/app/core/config.py - Configuration loader implementation (348 lines)
  • api-service/config.example.yaml - Comprehensive YAML template with inline documentation (169 lines)
  • api-service/.env.example - Environment variable reference with usage examples (107 lines)
  • api-service/app/core/__init__.py - Public API exports (Config, load_config, get_config)
  • api-service/tests/unit/core/test_config.py - Complete test suite (496 lines, 26 tests)
  • docs/dev/plans/EPIC-4-2-config-loader.md - Implementation plan

Testing

  • 26 unit tests pass (100% pass rate)
  • Coverage: 96.38% (exceeds 90% requirement)
  • All quality checks pass (ruff format, ruff check, pyright)
  • Tests cover:
    • YAML configuration loading
    • Environment variable overrides
    • Validation (missing fields, invalid types, invalid values)
    • Singleton pattern enforcement
    • Dot-notation access (get() method)
    • Property accessors (database, vault, ai, etc.)
    • Error handling with helpful messages
    • Path expansion and creation

API Usage

from app.core.config import load_config, get_config

# Load at startup (once)
config = load_config("config.yaml")

# Access throughout application
db_url = get_config().database["url"]
vault_path = get_config().get("vault.path")
ai_model = get_config().get("ai.classification.model", "gemini-1.5-flash")

Environment Variable Override Examples

# Override database URL
export DATABASE_URL=postgresql://prod_user:prod_pass@prod-db:5432/noosphere

# Override AI model
export AI_MODEL=gpt-4o-mini

# Override log level
export LOG_LEVEL=ERROR

Validation Features

Configuration validation ensures:

  • ✅ Required fields present (database.url is mandatory)
  • ✅ Valid value types (ports are integers, log levels are valid)
  • ✅ Valid value ranges (positive integers for pool sizes, ports 1-65535)
  • ✅ Path existence (vault directory created if missing with warning)
  • ✅ Helpful error messages guide users to fix problems

Security Practices

  • ✅ Secrets (API keys, passwords) via environment variables only
  • ✅ config.yaml and .env excluded from version control
  • ✅ config.example.yaml and .env.example tracked with placeholder values
  • ✅ No secrets logged or exposed in error messages

Verification Steps

cd api-service

# Install dependencies (if needed)
uv sync

# Run tests
export DATABASE_URL=postgresql://test:test@localhost/test
uv run pytest tests/unit/core/test_config.py -v

# Verify coverage
uv run pytest tests/unit/core/test_config.py --cov=app.core.config --cov-report=term-missing

# Manual verification
cp config.example.yaml config.yaml
cp .env.example .env
# Edit .env to set DATABASE_URL and API keys

# Test config loading
uv run python -c "
from app.core.config import load_config
config = load_config('config.yaml')
print(f'Database: {config.database[\"url\"]}')
print(f'Vault: {config.vault[\"path\"]}')
print(f'✓ Configuration loaded successfully')
"

Checklist

  • Code follows api-service conventions (app/ layout, FastAPI patterns)
  • api-service/AGENTS.md reviewed for context isolation
  • Tests written with 90%+ coverage (achieved 96%)
  • All quality checks pass (ruff, pyright)
  • Documentation comprehensive (.env.example, config.example.yaml, docstrings)
  • No secrets or sensitive data committed
  • Implementation plan posted to issue

Next Steps

After merge:

  1. ✅ EPIC-4-2 (Issue Configuration Loaders with Environment Variable Support #15) complete for Python/api-service
  2. 🔄 EPIC-4 progress: Configuration system foundation established
  3. ⏭️ Future: Integrate config loader into app startup (app/main.py when created)
  4. ⏭️ Future: Refactor app/db/session.py to use centralized config
  5. ⏭️ Separate: Rust configuration loader for sync-service (remaining portion of Issue Configuration Loaders with Environment Variable Support #15)

🤖 Generated with Claude Code

@github-actions github-actions Bot added service/api-service FastAPI Python backend service/database PostgreSQL schema/migrations service/docs Documentation only labels Jan 25, 2026
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @deserat, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a foundational configuration management system for the api-service, enabling flexible and secure application setup. It centralizes configuration logic, allowing settings to be defined in YAML files and dynamically overridden by environment variables, which is crucial for deploying applications across various environments. The system emphasizes early validation and clear error reporting to streamline development and deployment workflows.

Highlights

  • New Configuration System: Introduced a robust configuration loader for the api-service that merges YAML files with environment variable overrides, adhering to the 12-factor app methodology. This system prioritizes environment variables, then .env file values, followed by config.yaml, and finally hard-coded defaults.
  • Comprehensive Validation: Implemented fail-fast validation at application startup to ensure configuration integrity. This includes checks for required fields, valid data types, and value ranges, providing helpful error messages to guide users in resolving issues.
  • Flexible Path Handling: The configuration system now supports path expansion for ~ (home directory) and environment variables within file paths, particularly for the vault configuration, and can create missing vault directories with a warning.
  • Structured Configuration Sections: Configuration is organized into distinct sections for database, vault, scheduler, AI/LLM settings (including API keys and model selection), logging, and API server parameters, allowing for clear and manageable settings.
  • Extensive Test Coverage: A complete test suite with 26 unit tests has been added, achieving 96.38% code coverage. These tests verify YAML loading, environment variable overrides, validation logic, singleton pattern enforcement, and dot-notation access.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive and well-tested configuration loader for the API service, which is an excellent addition. It supports YAML files, .env files, and environment variable overrides, following the 12-factor app methodology. The implementation is solid and the extensive test suite is commendable.

My review focuses on improving robustness and maintainability. I've identified a critical issue where misconfigured environment variables could crash the application on startup, and I've provided a suggestion to handle this gracefully. I also have a couple of medium-severity suggestions to improve logging practices and reduce code repetition for better long-term maintainability.

Overall, this is a well-executed feature, and with a few adjustments, it will be even more robust.

Comment thread api-service/app/core/config.py Outdated
Comment on lines +87 to +89
if pool_size := os.getenv("DB_POOL_SIZE"):
self._ensure_section("database")
self._config["database"]["pool_size"] = int(pool_size)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The direct conversion of environment variables to integers using int() can lead to an unhandled ValueError if the environment variable is set to a non-integer string (e.g., DB_POOL_SIZE="abc"). This would crash the application on startup with a generic traceback, which is not user-friendly.

It's better to wrap the conversion in a try...except block and raise a ValueError with a more informative message, guiding the user to fix the configuration.

This same issue applies to other integer conversions in this method for DB_MAX_OVERFLOW (line 90), API_PORT (line 127), SURFACING_INTERVAL_SECONDS (line 135), and SCHEDULER_BATCH_SIZE (line 138).

Suggested change
if pool_size := os.getenv("DB_POOL_SIZE"):
self._ensure_section("database")
self._config["database"]["pool_size"] = int(pool_size)
if pool_size_str := os.getenv("DB_POOL_SIZE"):
self._ensure_section("database")
try:
self._config["database"]["pool_size"] = int(pool_size_str)
except ValueError:
raise ValueError(f"Invalid value for DB_POOL_SIZE: '{pool_size_str}'. Must be an integer.")

# Validate configuration
self._validate()

def _apply_env_overrides(self) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method is quite long and contains a lot of repetitive logic for handling each environment variable. This can make it harder to maintain and add new configuration variables in the future.

Consider refactoring this by using a data structure (like a list of tuples or a dictionary) to map environment variables to their corresponding configuration paths and types. This would allow you to loop through the mapping and apply the overrides in a more generic and concise way.

For example:

_ENV_OVERRIDES = {
    "DATABASE_URL": ("database.url", str),
    "DB_POOL_SIZE": ("database.pool_size", int),
    "DB_MAX_OVERFLOW": ("database.max_overflow", int),
    "VAULT_PATH": ("vault.path", str),
    # ... and so on
}

def _apply_env_overrides(self) -> None:
    # ...
    for env_var, (path, cast) in _ENV_OVERRIDES.items():
        if value := os.getenv(env_var):
            # Logic to set value using path and cast
            # This would also be a good place to centralize the try/except for casting
            ...

This approach would make the code more declarative and easier to extend.

Comment thread api-service/app/core/config.py Outdated

if not vault_path.exists():
# Create vault directory with warning
print(f"Warning: Vault path does not exist, creating: {vault_path}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using print() for warnings is generally discouraged in library or application code, as it writes directly to standard output and cannot be controlled, redirected, or formatted by a logging configuration. It's better to use the logging module. This allows the application that uses this config loader to control the log level, format, and destination (e.g., file, console, remote service).

To implement this, you'll need to:

  1. Import the logging module at the top of the file.
  2. Initialize a module-level logger: logger = logging.getLogger(__name__).
  3. Change this print() call to logger.warning(...).

Note: This change will require updating test_config_validation_vault_path_created in test_config.py to use the caplog fixture instead of capsys to assert that the warning was logged.

Suggested change
print(f"Warning: Vault path does not exist, creating: {vault_path}")
logger.warning(f"Vault path does not exist, creating: {vault_path}")

… support

- What: Created Config class with YAML + environment variable support
- Why: Foundation for environment-specific configuration (Issue #15)
- How: Python-dotenv for .env, PyYAML for config.yaml, validation

**Review Fixes (Gemini Code Assist feedback)**:

1. CRITICAL: Fixed unhandled ValueError in integer conversions
   - Added _parse_int() helper with try/except and helpful error messages
   - Applied to all 5 integer conversions (DB_POOL_SIZE, DB_MAX_OVERFLOW,
     API_PORT, SURFACING_INTERVAL_SECONDS, SCHEDULER_BATCH_SIZE)
   - Added 5 new tests for invalid integer values

2. MEDIUM: Refactored repetitive logic in _apply_env_overrides
   - Created declarative _ENV_VAR_MAPPINGS structure
   - Reduced _apply_env_overrides from ~70 lines to ~20 lines
   - Added _set_nested() helper for dot-notation path setting
   - Centralized type conversion and error handling

3. MEDIUM: Replaced print() with proper logging
   - Added logging module and module-level logger
   - Changed vault path warning to logger.warning()
   - Updated test to use caplog fixture instead of capsys

Acceptance criteria addressed:
- [x] Config loads YAML configuration from file
- [x] python-dotenv loads .env file
- [x] Environment variables override YAML values
- [x] Property accessors (database, vault, ai, etc.)
- [x] get() method with dot notation
- [x] Singleton pattern via load_config() and get_config()
- [x] Configuration validation with helpful errors
- [x] Robust error handling for invalid values

Testing:
- Unit tests for all configuration features (26 tests)
- Error handling tests for invalid integers (5 tests)
- Coverage: 94% (target: 90%+)
- All quality checks pass (ruff format, ruff check, pyright)

Files created:
- app/core/config.py - Configuration loader (331 lines)
- config.example.yaml - Example configuration (169 lines)
- .env.example - Environment variable documentation (107 lines)
- tests/unit/core/test_config.py - Test suite (31 tests, 562 lines)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@deserat
deserat force-pushed the feature/EPIC-4-2-config-loader branch from 5418ca2 to b1f5180 Compare January 25, 2026 22:15
@deserat

deserat commented Jan 25, 2026

Copy link
Copy Markdown
Owner Author

@gemini-code-assist Thank you for the thorough review! All issues have been addressed:

1. ✅ CRITICAL: Integer conversion error handling

Fixed: Added _parse_int() helper method with try/except and helpful error messages

  • Applied to all 5 integer conversions:
    • DB_POOL_SIZE (line 160)
    • DB_MAX_OVERFLOW (line 162)
    • API_PORT (line 173)
    • SURFACING_INTERVAL_SECONDS (line 178)
    • SCHEDULER_BATCH_SIZE (line 181)
  • Added 5 new tests for invalid integer values
  • Now raises: ValueError: Invalid value for DB_POOL_SIZE: 'abc'. Must be an integer.

2. ✅ MEDIUM: Refactored repetitive logic

Fixed: Created declarative _ENV_VAR_MAPPINGS structure

  • Reduced _apply_env_overrides() from ~70 lines to ~20 lines (net -50 lines)
  • Added _set_nested() helper for dot-notation path setting
  • Centralized type conversion and error handling
  • Benefits:
    • Easier to add new environment variables (just add to mapping)
    • Self-documenting configuration
    • Consistent error handling

3. ✅ MEDIUM: Replaced print() with logging

Fixed: Added proper logging infrastructure

  • Added import logging and module-level logger
  • Changed vault path warning to logger.warning()
  • Updated test to use caplog fixture instead of capsys
  • Warnings now respect logging configuration

Verification

All changes maintain backward compatibility and improve code quality:

  • Tests: 31/31 passing (26 existing + 5 new)
  • Coverage: 94% (target: 90%+)
  • Ruff format: Pass
  • Ruff check: Pass (app/core/config.py)
  • Pyright: Pass (0 errors)

The code is now more robust, maintainable, and follows Python best practices.

@gemini-code-assist

Copy link
Copy Markdown

Thank you, @deserat, for the detailed update and for addressing all the points raised in the review! I appreciate your thoroughness in implementing the suggested changes.

It's great to see the improvements:

  1. CRITICAL: Integer conversion error handling: The addition of the _parse_int() helper and the new tests significantly enhance the robustness of the configuration loading, preventing potential runtime errors with clear, user-friendly messages.
  2. MEDIUM: Refactored repetitive logic: The _ENV_VAR_MAPPINGS structure and _set_nested() helper are excellent refactorings that greatly improve the maintainability and readability of the environment variable override logic. This is a substantial improvement.
  3. MEDIUM: Replaced print() with logging: Switching to the logging module for warnings is a best practice that allows for better control and integration with application-wide logging strategies.

Your verification steps confirm that the changes are well-tested and maintain high code quality. Excellent work!

@deserat
deserat merged commit 3f921a3 into develop Jan 25, 2026
1 check passed
@deserat
deserat deleted the feature/EPIC-4-2-config-loader branch January 25, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

service/api-service FastAPI Python backend service/database PostgreSQL schema/migrations service/docs Documentation only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant