Status: Accepted Version/Date: v1.0 / 2026-06-15
Centralized Path Single Source of Truth (SSOT) Configuration and AST Quality Gates
Establish a centralized path management architecture using config/paths.yml as the Single Source of Truth (SSOT), dynamically populated as Path objects in Python via paths.py relative to a deterministic .project_root anchor, and strictly enforced across the entire codebase using AST quality gate unit tests.
A common anti-pattern in complex data engineering projects is to resolve paths in-place inside individual source modules. Typically, this looks like:
# Fragile, hardcoded path resolution in application logic
from pathlib import Path
DATA_DIR = Path(__file__).resolve().parents[2] / "data" / "raw"This ad-hoc approach has severe architectural flaws:
- Runtime Portability Failure: When executing code under different runtimes (such as a local shell, inside Docker containers, within Apache Spark clusters, or triggered by an orchestrator like Airflow), the working directory and file hierarchy change. Hardcoded relative traversals (via multi-level parent directory traversal) break immediately when the execution context changes.
- Refactoring Nightmare: If the project directory layout changes (e.g., relocating
fms_metadata/or.data/folders), developers must manually search and update every single file that traverses or constructs paths. - Untestable Code: Mocking filesystem operations or redirecting paths to isolated temporary test directories is extremely difficult when paths are constructed locally inside target functions.
- Configuration Drift: Having multiple files declare where configurations or data directories reside inevitably leads to silent drift, where different jobs expect data in slightly different locations.
To eliminate this fragility, we establish a centralized, declarative paths configuration registry. A base root directory is located deterministically using a file-based anchor (.project_root) and environment variable fallbacks, while all subdirectories and configuration files are registered in a single YAML file and exported dynamically.
- Single Source of Truth (SSOT): Ensure every directory and file path in the workspace is defined in exactly one place.
- Portability & Environment Agnosticism: Guarantee path resolution works identically in development, CI, testing, and production (Docker/Spark).
- Refactoring Ease: Allow updating the entire directory hierarchy layout by changing a single line of configuration.
- Strict Compliance: Enforce that developers cannot bypass the SSOT by writing raw paths or in-place resolutions in the codebase.
- Alternative A: Hardcoded Relative Path Resolution — Resolve paths in-place relative to module files (
__file__) or current working directory (os.getcwd()).- Pros: Simple to write initially, zero dependencies.
- Cons: Extremely fragile, breaks under Spark/Docker environments, impossible to easily refactor directories, hard to mock during tests.
- Alternative B: Pure Environment Variable Configuration — Inject all paths as environment variables via shell scripts or
.envfiles.- Pros: Decoupled from codebase structure.
- Cons: Clutters shell scripts, no static analysis validation, prone to typos, high configuration boilerplate.
- Alternative C: Centralized Declarative Path Registry + AST Quality Gates (SELECTED) — Declare paths in
config/paths.yml, load them dynamically in Python, locate the project root using a.project_rootanchor, and audit compliance with automated tests.- Pros: Single configuration file, absolute layout portability, clean type-safe
Pathimports, zero boilerplate, and 100% automated enforcement. - Cons: Dynamic module attribute generation requires fallback hooks (
__getattr__) to satisfy static type checkers and IDE autocompletion tools.
- Pros: Single configuration file, absolute layout portability, clean type-safe
| Model / Option | Portability & Robustness (Weight: 35%) | Maintainability & DRY (Weight: 30%) | Code Cleanliness (Weight: 20%) | Enforcement & Safety (Weight: 15%) | Total Score | Decision |
|---|---|---|---|---|---|---|
| Alternative C (Selected) | 10/10 | 10/10 | 9/10 | 10/10 | 9.80 | ✅ Selected |
| Alternative B | 7/10 | 6/10 | 5/10 | 3/10 | 5.70 | Rejected |
| Alternative A | 1/10 | 2/10 | 4/10 | 0/10 | 1.75 | Rejected |
We will adopt Alternative C (Centralized Declarative Path Registry + AST Quality Gates).
- All paths are defined relative to the project root in
config/paths.yml. - The project root is resolved deterministically by
project_root.pyusing the.project_rootmarker file or aPROJECT_ROOTenvironment override. - The
paths.pylibrary interface loads the YAML file and dynamically populates its module namespace. - We implement
test_paths_ssot_quality_gate.pyto statically audit all files, ensuring no raw path constructions or standard library resolutions bypass this registry.
- FR-1: The system must locate the project root directory regardless of whether it is run from a terminal shell, a test runner, or inside containerized workloads.
- FR-2: The application must expose all configured paths as standard
pathlib.Pathobjects to ensure cross-platform compatibility (Windows/Linux/macOS).
- NFR-1 (Maintainability): Modifying any directory path must only require changing the configuration in
config/paths.yml. - NFR-2 (Testability): All path variables must be mockable/overrideable during unit and integration test executions.
- NFR-3 (Strict Verification): The workspace layout registry compliance must be validated automatically as part of the test suite.
- PR-1: Path resolution must execute in sub-millisecond times, caching references to avoid redundant lookup processes.
- IR-1: The registry configuration must run transparently across CLI, local unit test runner sessions, Docker mounts, and PySpark workers.
- ADR-001 (Centralized YAML Configuration): Establishes the standard configuration layout patterns under
config/andconfig_env/.
In src/entsoe_pipeline/config/core/project_root.py:
"""Project root lookup implementation core module."""
from __future__ import annotations
import os
from pathlib import Path
def find_project_root() -> Path:
"""Locates the project root directory deterministically.
Resolves the root folder using a three-tier fallback strategy:
1. Prioritizes the 'PROJECT_ROOT' environment variable (useful in Docker/Airflow).
2. Searches parent directories upward for the '.project_root' anchor file.
3. Falls back to static parent calculation relative to this file's path.
"""
if env_root := os.getenv("PROJECT_ROOT"):
return Path(env_root)
current_file = Path(__file__).resolve()
for parent in current_file.parents:
if (parent / ".project_root").exists():
return parent
return current_file.parents[4]In src/entsoe_pipeline/config/paths.py:
import typing
from entsoe_pipeline.config.core.project_root import find_project_root
from entsoe_pipeline.config.config_loader import load_paths_config
PROJECT_ROOT = find_project_root()
PATHS_YML = PROJECT_ROOT / "config" / "paths.yml"
# Load paths dynamically from the SSOT configuration file
_paths_data = load_paths_config(PROJECT_ROOT)
# Populate module namespace dynamically to establish SSOT exports
for _key, _rel_val in _paths_data.items():
globals()[_key] = PROJECT_ROOT / _rel_val
# Expose constants for package-wide utilization
__all__ = ["PROJECT_ROOT"]
__all__.extend(list(_paths_data.keys()))
def __getattr__(name: str) -> typing.Any:
"""Allow dynamic attributes for static type checkers.
This magic method ensures that IDEs, static linters, and type checkers (like
mypy/pyright) recognize the dynamically populated path constants as valid
module-level attributes, preventing false-positive lint warnings.
"""
if name in __all__:
return globals().get(name)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")In config/paths.yml:
# ENTSO-E Metadata Pipeline Path Configurations
# This file is the Single Source of Truth (SSOT) for all workspace paths.
# All path values are declared relative to the PROJECT_ROOT directory.
DATA_DIR: ".data"
TESTS_DIR: "tests"
ADR_DIR: "docs/adr"
API_LIMITS_YML: "config/entsoe_api_limits.yml"
CONFIG_DIR: "config_env"
CONFIG_EXAMPLE_DIR: "config_env_example"
ENV_FILE: ".env"
FMS_METADATA_DIR: "fms_metadata"
OVERVIEW_YML: "fms_metadata/overview.yml"
OVERVIEW_TREE_YML: "fms_metadata/overview_tree.yml"
EXPORT_LOG_YML: "fms_metadata/export_log.yml"
LANDING_BUCKET_SCHEMA_YML: "config/entsoe_fms_folder_schema.yml"To ensure absolute adherence to our paths convention, we perform two layers of validation:
- Convention Compliance (Quality Gate): We statically parse all
.pyfiles using regular expressions to audit code patterns. Any script attempting to resolve parent hierarchies usingPath(__file__), checkingos.getcwd(), or instantiating hardcoded path strings pointing to workspace folder targets will fail this check. - Symmetry and Parity Tests: We ensure that every uppercase constant dynamically exported by
paths.pymatches the declarative list registered inpaths.yml, preventing undocumented path constants.
In tests/test_paths_ssot_quality_gate.py:
import re
from pathlib import Path
import pytest
from entsoe_pipeline import SRC_DIR, TESTS_DIR
EXEMPT_FILES = {"paths.py", "project_root.py", "test_paths.py", "test_paths_ssot_quality_gate.py"}
def get_python_files():
# Recursively yield all source and test modules, excluding EXEMPT_FILES
return [p for p in SRC_DIR.rglob("*.py") if p.name not in EXEMPT_FILES]
@pytest.mark.parametrize("file_path", get_python_files())
def test_no_raw_paths_or_parent_traversal(file_path: Path):
"""Quality gate verifying that modules import path constants from paths.py."""
content = file_path.read_text(encoding="utf-8")
violations = []
# Audit for in-place relative traverses
if re.search(r"Path\(\s*__file__\s*\)", content):
violations.append("Found reference to Path(__file__). Centralize path in paths.py")
if re.search(r"PROJECT_ROOT\s*/", content):
violations.append("Direct raw path concatenation using PROJECT_ROOT. Export from paths.py")
if re.search(r'Path\(\s*["\'](?:\.env|\.data|config_env)["\']\s*\)', content):
violations.append("Direct instantiation of hardcoded repository paths. Import from paths.py")
assert not violations, f"Path violations found in {file_path.name}:\n" + "\n".join(violations)- Absolute Portability: Running tests or pipeline runs works natively across local laptops, staging setups, Docker environments, and PySpark clusters without path failures.
- Easy Maintenance: Moving a directory layout requires changing exactly one YAML line, rather than editing dozens of hardcoded paths.
- Improved Testability: Unit tests can safely patch or redirect specific directories (like the landing bucket schema path) by simply altering
paths.LANDING_BUCKET_SCHEMA_YMLwithout complex filesystem mocking. - No Hardcoding Drift: Ensures new developers cannot introduce ad-hoc layouts, enforcing clean development hygiene.
- Static Analyzer Workarounds: Dynamic namespace injection using
globals()bypasses standard static code compilation checks. To ensure IDE autocompletions and linters still recognize the constants, we had to introduce__getattr__fallback hooks and declare them in package exports. - Overhead in Setup: Adding a new directory path requires adding it to both
paths.ymland updating the coverage test dictionary intest_paths.py.
- New paths must always be defined in
config/paths.ymland mirrored in theEXPECTED_PATHSmapping insidetests/test_paths.py. - Any exceptions/exemptions to the quality gate (such as utility scripts or path-specific test suites) must be explicitly listed in the
EXEMPT_FILESset intests/test_paths_ssot_quality_gate.py.
- Infrastructure:
.project_rootempty anchor file located at the project root directory. - Libraries:
pathlib(Python Standard Library),ruamel.yaml/yaml(Parser).
- ADR-001: Centralized YAML Configuration
- Python PEP 562 -- Module getattr and dir
- Twelve-Factor App guidelines on configuration and path portability
- v1.0 (2026-06-15): Initial accepted version detailing centralized dynamic YAML paths configuration and AST quality gates.

