File size: 5,519 Bytes
c2a61b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
"""

Configuration Management Module



Provides YAML-based configuration loading with validation and path resolution.

All system parameters are centralized in config files, avoiding hard-coded values.



Design Decisions:

- YAML format for human readability and easy editing

- Relative paths resolved from project root

- Deep merge support for config overrides

- Validation of required fields



Time Complexity: O(n) where n = number of config entries

Space Complexity: O(n) for config dictionary storage

"""

import os
from pathlib import Path
from typing import Any, Dict, Optional
import yaml


def get_project_root() -> Path:
    """

    Find the project root directory.

    

    Strategy: Walk up from this file until we find a directory containing

    'config' folder or 'requirements.txt' (project markers).

    

    Returns:

        Path to project root directory

        

    Raises:

        RuntimeError: If project root cannot be determined

    """
    current = Path(__file__).resolve().parent
    
    # Walk up directory tree
    for _ in range(10):  # Limit search depth
        if (current / "config").is_dir() or (current / "requirements.txt").is_file():
            return current
        parent = current.parent
        if parent == current:  # Reached filesystem root
            break
        current = parent
    
    # Fallback: assume we're in src/utils, go up two levels
    fallback = Path(__file__).resolve().parent.parent.parent
    if fallback.is_dir():
        return fallback
    
    raise RuntimeError(
        "Could not determine project root. "
        "Ensure you're running from within the project directory."
    )


def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
    """

    Load configuration from YAML file.

    

    Args:

        config_path: Path to config file. If None, loads default.yaml

        

    Returns:

        Configuration dictionary with all parameters

        

    Raises:

        FileNotFoundError: If config file doesn't exist

        yaml.YAMLError: If config file is malformed

    """
    project_root = get_project_root()
    
    if config_path is None:
        config_path = project_root / "config" / "default.yaml"
    else:
        config_path = Path(config_path)
        if not config_path.is_absolute():
            config_path = project_root / config_path
    
    if not config_path.exists():
        raise FileNotFoundError(f"Configuration file not found: {config_path}")
    
    with open(config_path, "r", encoding="utf-8") as f:
        config = yaml.safe_load(f)
    
    # Resolve relative paths to absolute paths
    config = _resolve_paths(config, project_root)
    
    # Add project root to config for convenience
    config["_project_root"] = str(project_root)
    
    return config


def _resolve_paths(config: Dict[str, Any], project_root: Path) -> Dict[str, Any]:
    """

    Resolve relative paths in config to absolute paths.

    

    Identifies path-like config values and resolves them relative to project root.

    Path-like values are those ending in '_dir' or '_path'.

    

    Args:

        config: Configuration dictionary

        project_root: Project root path for resolution

        

    Returns:

        Config with resolved paths

    """
    path_suffixes = ("_dir", "_path", "_file")
    
    def resolve_recursive(obj: Any, parent_key: str = "") -> Any:
        if isinstance(obj, dict):
            return {
                k: resolve_recursive(v, k) 
                for k, v in obj.items()
            }
        elif isinstance(obj, list):
            return [resolve_recursive(item, parent_key) for item in obj]
        elif isinstance(obj, str):
            # Check if this looks like a path
            if any(parent_key.endswith(suffix) for suffix in path_suffixes):
                path = Path(obj)
                if not path.is_absolute():
                    return str(project_root / path)
            return obj
        else:
            return obj
    
    return resolve_recursive(config)


def save_config(config: Dict[str, Any], save_path: str) -> None:
    """

    Save configuration to YAML file.

    

    Useful for saving experiment configurations for reproducibility.

    

    Args:

        config: Configuration dictionary

        save_path: Path to save config file

    """
    save_path = Path(save_path)
    save_path.parent.mkdir(parents=True, exist_ok=True)
    
    # Remove internal keys before saving
    config_to_save = {
        k: v for k, v in config.items() 
        if not k.startswith("_")
    }
    
    with open(save_path, "w", encoding="utf-8") as f:
        yaml.dump(config_to_save, f, default_flow_style=False, sort_keys=False)


def merge_configs(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
    """

    Deep merge two configuration dictionaries.

    

    Override values take precedence. Useful for command-line overrides.

    

    Args:

        base: Base configuration

        override: Override values

        

    Returns:

        Merged configuration

    """
    result = base.copy()
    
    for key, value in override.items():
        if key in result and isinstance(result[key], dict) and isinstance(value, dict):
            result[key] = merge_configs(result[key], value)
        else:
            result[key] = value
    
    return result