Skip to content

Commit 37e99ef

Browse files
zhravanraghavyuva
andauthored
feat: convert config class to functional (#641)
Co-authored-by: Raghavendra Bhat <[email protected]>
1 parent 5f85177 commit 37e99ef

File tree

1 file changed

+99
-86
lines changed

1 file changed

+99
-86
lines changed

cli/app/utils/config.py

Lines changed: 99 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,115 @@
11
import os
22
import re
33
import sys
4+
from typing import Any, Dict, Optional
45

56
import yaml
67

78
from app.utils.message import MISSING_CONFIG_KEY_MESSAGE
89

910

10-
class Config:
11-
def __init__(self, default_env="PRODUCTION"):
12-
self.default_env = default_env
13-
self._yaml_config = None
14-
self._user_config_file = None
15-
self._cache = {}
11+
def get_config_file_path(default_env: str = "PRODUCTION") -> str:
12+
"""Get the path to the config file based on environment"""
13+
config_file = "config.dev.yaml" if default_env.upper() == "DEVELOPMENT" else "config.prod.yaml"
14+
15+
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
16+
return os.path.join(sys._MEIPASS, "helpers", config_file)
17+
else:
18+
return os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../helpers", config_file))
19+
20+
21+
def load_config_file(config_file_path: str) -> Dict[str, Any]:
22+
"""Load YAML config file"""
23+
with open(config_file_path, "r") as f:
24+
return yaml.safe_load(f) or {}
25+
26+
27+
def get_active_config(user_config_file: Optional[str] = None, default_env: str = "PRODUCTION") -> Dict[str, Any]:
28+
"""Get the active config (user config if provided, else default)"""
29+
if user_config_file:
30+
if not os.path.exists(user_config_file):
31+
raise FileNotFoundError(f"Config file not found: {user_config_file}")
32+
return load_config_file(user_config_file)
33+
34+
config_file_path = get_config_file_path(default_env)
35+
return load_config_file(config_file_path)
36+
37+
38+
def get_env(default_env: str = "PRODUCTION") -> str:
39+
"""Get environment from ENV variable or default"""
40+
return os.environ.get("ENV", default_env)
41+
42+
43+
def is_development(default_env: str = "PRODUCTION") -> bool:
44+
"""Check if current environment is development"""
45+
return get_env(default_env).upper() == "DEVELOPMENT"
46+
47+
48+
def get_config_value(
49+
config: Dict[str, Any],
50+
path: str,
51+
cache: Optional[Dict[str, Any]] = None
52+
) -> Any:
53+
"""Get config value using dot notation path"""
54+
if cache is None:
55+
cache = {}
56+
57+
if path in cache:
58+
return cache[path]
59+
60+
keys = path.split(".")
61+
value = config
62+
for key in keys:
63+
if isinstance(value, dict) and key in value:
64+
value = value[key]
65+
else:
66+
raise KeyError(MISSING_CONFIG_KEY_MESSAGE.format(path=path, key=key))
67+
68+
if isinstance(value, str):
69+
value = expand_env_placeholders(value)
70+
71+
cache[path] = value
72+
return value
1673

17-
config_file = "config.dev.yaml" if default_env.upper() == "DEVELOPMENT" else "config.prod.yaml"
1874

19-
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
20-
self._yaml_path = os.path.join(sys._MEIPASS, "helpers", config_file)
21-
else:
22-
self._yaml_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../helpers", config_file))
23-
24-
def get_env(self):
25-
return os.environ.get("ENV", self.default_env)
26-
27-
def is_development(self):
28-
return self.get_env().upper() == "DEVELOPMENT"
29-
30-
def load_user_config(self, config_file: str):
31-
"""Set user config file to replace default config."""
32-
if config_file and not os.path.exists(config_file):
33-
raise FileNotFoundError(f"Config file not found: {config_file}")
34-
self._user_config_file = config_file
35-
self._yaml_config = None
36-
self._cache = {}
37-
38-
def _get_active_config(self):
39-
"""Get the active config (user config if provided, else default)."""
40-
if self._user_config_file:
41-
if self._yaml_config is None:
42-
with open(self._user_config_file, "r") as f:
43-
self._yaml_config = yaml.safe_load(f)
44-
return self._yaml_config
45-
46-
if self._yaml_config is None:
47-
with open(self._yaml_path, "r") as f:
48-
self._yaml_config = yaml.safe_load(f)
49-
return self._yaml_config
50-
51-
def get(self, path: str):
52-
"""Get config value using dot notation path."""
53-
if path in self._cache:
54-
return self._cache[path]
55-
56-
config = self._get_active_config()
57-
keys = path.split(".")
58-
for key in keys:
59-
if isinstance(config, dict) and key in config:
60-
config = config[key]
61-
else:
62-
raise KeyError(MISSING_CONFIG_KEY_MESSAGE.format(path=path, key=key))
63-
64-
if isinstance(config, str):
65-
config = expand_env_placeholders(config)
66-
67-
self._cache[path] = config
68-
return config
69-
70-
def get_service_env_values(self, service_env_path: str):
71-
"""Get service environment values as a dictionary."""
72-
env_config = self.get(service_env_path)
73-
if not isinstance(env_config, dict):
74-
raise ValueError(f"Expected dictionary at path '{service_env_path}'")
75-
return {key: expand_env_placeholders(value) if isinstance(value, str) else value for key, value in env_config.items()}
76-
77-
def load_yaml_config(self):
78-
"""Return the active config dict (for backward compatibility)."""
79-
return self._get_active_config()
80-
81-
def get_yaml_value(self, path: str):
82-
"""Alias for get() for backward compatibility."""
83-
return self.get(path)
84-
85-
def unflatten_config(self, flattened_config: dict) -> dict:
86-
"""Convert flattened config back to nested structure."""
87-
nested = {}
88-
for key, value in flattened_config.items():
89-
keys = key.split(".")
90-
current = nested
91-
for k in keys[:-1]:
92-
if k not in current:
93-
current[k] = {}
94-
current = current[k]
95-
current[keys[-1]] = value
96-
return nested
75+
def get_service_env_values(config: Dict[str, Any], service_env_path: str) -> Dict[str, Any]:
76+
"""Get service environment values as a dictionary"""
77+
env_config = get_config_value(config, service_env_path)
78+
if not isinstance(env_config, dict):
79+
raise ValueError(f"Expected dictionary at path '{service_env_path}'")
80+
return {key: expand_env_placeholders(value) if isinstance(value, str) else value for key, value in env_config.items()}
81+
82+
83+
def load_yaml_config(user_config_file: Optional[str] = None, default_env: str = "PRODUCTION") -> Dict[str, Any]:
84+
"""Return the active config dict (for backward compatibility)"""
85+
return get_active_config(user_config_file, default_env)
86+
87+
88+
def get_yaml_value(
89+
config: Dict[str, Any],
90+
path: str,
91+
cache: Optional[Dict[str, Any]] = None
92+
) -> Any:
93+
"""Alias for get_config_value() for backward compatibility"""
94+
return get_config_value(config, path, cache)
95+
96+
97+
def unflatten_config(flattened_config: dict) -> dict:
98+
"""Convert flattened config back to nested structure"""
99+
nested = {}
100+
for key, value in flattened_config.items():
101+
keys = key.split(".")
102+
current = nested
103+
for k in keys[:-1]:
104+
if k not in current:
105+
current[k] = {}
106+
current = current[k]
107+
current[keys[-1]] = value
108+
return nested
97109

98110

99111
def expand_env_placeholders(value: str) -> str:
100-
# Expand environment placeholders in the form ${ENV_VAR:-default}
112+
"""Expand environment placeholders in the form ${ENV_VAR:-default}"""
101113
# Supports nested expansions like ${VAR1:-${VAR2:-default}}
102114
pattern = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?}")
103115
max_iterations = 10 # Prevent infinite loops
@@ -116,6 +128,7 @@ def replacer(match):
116128
return value
117129

118130

131+
# Config path constants
119132
VIEW_ENV_FILE = "services.view.env.VIEW_ENV_FILE"
120133
API_ENV_FILE = "services.api.env.API_ENV_FILE"
121134
DEFAULT_REPO = "clone.repo"

0 commit comments

Comments
 (0)