diagram-to-iac 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- diagram_to_iac/__init__.py +34 -8
- diagram_to_iac/agents/demonstrator_langgraph/agent.py +12 -8
- diagram_to_iac/agents/git_langgraph/agent.py +74 -24
- diagram_to_iac/agents/hello_langgraph/agent.py +69 -13
- diagram_to_iac/agents/policy_agent/agent.py +41 -18
- diagram_to_iac/agents/supervisor_langgraph/agent.py +70 -25
- diagram_to_iac/agents/terraform_langgraph/agent.py +75 -27
- diagram_to_iac/core/config_loader.py +281 -0
- diagram_to_iac/core/memory.py +7 -2
- diagram_to_iac/tools/api_utils.py +61 -20
- diagram_to_iac/tools/git/git.py +69 -18
- diagram_to_iac/tools/sec_utils.py +248 -35
- diagram_to_iac/tools/shell/shell.py +89 -32
- diagram_to_iac/tools/tf/terraform.py +43 -32
- {diagram_to_iac-1.0.1.dist-info → diagram_to_iac-1.0.3.dist-info}/METADATA +3 -3
- {diagram_to_iac-1.0.1.dist-info → diagram_to_iac-1.0.3.dist-info}/RECORD +19 -18
- {diagram_to_iac-1.0.1.dist-info → diagram_to_iac-1.0.3.dist-info}/WHEEL +0 -0
- {diagram_to_iac-1.0.1.dist-info → diagram_to_iac-1.0.3.dist-info}/entry_points.txt +0 -0
- {diagram_to_iac-1.0.1.dist-info → diagram_to_iac-1.0.3.dist-info}/top_level.txt +0 -0
@@ -42,6 +42,7 @@ from diagram_to_iac.tools.llm_utils.router import get_llm, LLMRouter
|
|
42
42
|
from diagram_to_iac.core.agent_base import AgentBase
|
43
43
|
from diagram_to_iac.core.memory import create_memory, LangGraphMemoryAdapter
|
44
44
|
from diagram_to_iac.services.observability import log_event
|
45
|
+
from diagram_to_iac.core.config_loader import get_config, get_config_value
|
45
46
|
from .parser import classify_terraform_error
|
46
47
|
|
47
48
|
|
@@ -125,36 +126,54 @@ class TerraformAgent(AgentBase):
|
|
125
126
|
# Store memory type for tool initialization
|
126
127
|
self.memory_type = memory_type
|
127
128
|
|
128
|
-
# Load configuration
|
129
|
+
# Load configuration using centralized system
|
129
130
|
if config_path is None:
|
130
131
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
131
132
|
config_path = os.path.join(base_dir, "config.yaml")
|
132
133
|
self.logger.debug(f"Default config path set to: {config_path}")
|
133
134
|
|
134
135
|
try:
|
135
|
-
|
136
|
-
|
137
|
-
|
136
|
+
# Use centralized configuration loading with hierarchical merging
|
137
|
+
base_config = get_config()
|
138
|
+
|
139
|
+
# Load agent-specific config if provided
|
140
|
+
agent_config = {}
|
141
|
+
if config_path and os.path.exists(config_path):
|
142
|
+
with open(config_path, 'r') as f:
|
143
|
+
agent_config = yaml.safe_load(f) or {}
|
144
|
+
|
145
|
+
# Deep merge base config with agent-specific overrides
|
146
|
+
self.config = self._deep_merge(base_config, agent_config)
|
147
|
+
self.logger.info(f"Configuration loaded successfully from centralized system")
|
148
|
+
if "terraform_token_env" not in self.config:
|
149
|
+
self.config["terraform_token_env"] = "TFE_TOKEN"
|
150
|
+
except Exception as e:
|
151
|
+
self.logger.warning(f"Failed to load configuration via centralized system: {e}. Using fallback.")
|
152
|
+
# Fallback to direct YAML loading for backward compatibility
|
153
|
+
try:
|
154
|
+
with open(config_path, "r") as f:
|
155
|
+
self.config = yaml.safe_load(f)
|
156
|
+
if self.config is None:
|
157
|
+
self.logger.warning(
|
158
|
+
f"Configuration file at {config_path} is empty. Using defaults."
|
159
|
+
)
|
160
|
+
self._set_default_config()
|
161
|
+
else:
|
162
|
+
self.logger.info(
|
163
|
+
f"Configuration loaded successfully from {config_path}"
|
164
|
+
)
|
165
|
+
if "terraform_token_env" not in self.config:
|
166
|
+
self.config["terraform_token_env"] = "TFE_TOKEN"
|
167
|
+
except FileNotFoundError:
|
138
168
|
self.logger.warning(
|
139
|
-
f"Configuration file at {config_path}
|
169
|
+
f"Configuration file not found at {config_path}. Using defaults."
|
140
170
|
)
|
141
171
|
self._set_default_config()
|
142
|
-
|
143
|
-
self.logger.
|
144
|
-
f"
|
172
|
+
except yaml.YAMLError as e:
|
173
|
+
self.logger.error(
|
174
|
+
f"Error parsing YAML configuration: {e}. Using defaults.", exc_info=True
|
145
175
|
)
|
146
|
-
|
147
|
-
self.config["terraform_token_env"] = "TFE_TOKEN"
|
148
|
-
except FileNotFoundError:
|
149
|
-
self.logger.warning(
|
150
|
-
f"Configuration file not found at {config_path}. Using defaults."
|
151
|
-
)
|
152
|
-
self._set_default_config()
|
153
|
-
except yaml.YAMLError as e:
|
154
|
-
self.logger.error(
|
155
|
-
f"Error parsing YAML configuration: {e}. Using defaults.", exc_info=True
|
156
|
-
)
|
157
|
-
self._set_default_config()
|
176
|
+
self._set_default_config()
|
158
177
|
|
159
178
|
# Initialize enhanced LLM router
|
160
179
|
self.llm_router = LLMRouter()
|
@@ -178,20 +197,49 @@ class TerraformAgent(AgentBase):
|
|
178
197
|
self.logger.info("TerraformAgent initialized successfully")
|
179
198
|
|
180
199
|
def _set_default_config(self):
|
181
|
-
"""Set default configuration values."""
|
200
|
+
"""Set default configuration values using centralized system."""
|
182
201
|
self.logger.info("Setting default configuration for TerraformAgent")
|
183
202
|
self.config = {
|
184
|
-
"llm": {
|
203
|
+
"llm": {
|
204
|
+
"model_name": get_config_value("ai.default_model", "gpt-4o-mini"),
|
205
|
+
"temperature": get_config_value("ai.default_temperature", 0.1)
|
206
|
+
},
|
185
207
|
"routing_keys": {
|
186
|
-
"terraform_init": "ROUTE_TO_TF_INIT",
|
187
|
-
"terraform_plan": "ROUTE_TO_TF_PLAN",
|
188
|
-
"terraform_apply": "ROUTE_TO_TF_APPLY",
|
189
|
-
"open_issue": "ROUTE_TO_OPEN_ISSUE",
|
190
|
-
"end": "ROUTE_TO_END",
|
208
|
+
"terraform_init": get_config_value("routing.tokens.terraform_init", "ROUTE_TO_TF_INIT"),
|
209
|
+
"terraform_plan": get_config_value("routing.tokens.terraform_plan", "ROUTE_TO_TF_PLAN"),
|
210
|
+
"terraform_apply": get_config_value("routing.tokens.terraform_apply", "ROUTE_TO_TF_APPLY"),
|
211
|
+
"open_issue": get_config_value("routing.tokens.open_issue", "ROUTE_TO_OPEN_ISSUE"),
|
212
|
+
"end": get_config_value("routing.tokens.end", "ROUTE_TO_END"),
|
191
213
|
},
|
192
214
|
"terraform_token_env": "TFE_TOKEN",
|
215
|
+
"tools": {
|
216
|
+
"terraform": {
|
217
|
+
"timeout": get_config_value("network.terraform_timeout", 300),
|
218
|
+
"default_auto_approve": get_config_value("tools.terraform.default_auto_approve", True),
|
219
|
+
"default_plan_file": get_config_value("tools.terraform.default_plan_file", "plan.tfplan")
|
220
|
+
}
|
221
|
+
}
|
193
222
|
}
|
194
223
|
|
224
|
+
def _deep_merge(self, base: dict, overlay: dict) -> dict:
|
225
|
+
"""
|
226
|
+
Deep merge two dictionaries, with overlay taking precedence.
|
227
|
+
|
228
|
+
Args:
|
229
|
+
base: Base dictionary
|
230
|
+
overlay: Dictionary to overlay on base
|
231
|
+
|
232
|
+
Returns:
|
233
|
+
Merged dictionary
|
234
|
+
"""
|
235
|
+
result = base.copy()
|
236
|
+
for key, value in overlay.items():
|
237
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
238
|
+
result[key] = self._deep_merge(result[key], value)
|
239
|
+
else:
|
240
|
+
result[key] = value
|
241
|
+
return result
|
242
|
+
|
195
243
|
def _initialize_tools(self):
|
196
244
|
"""Initialize the Terraform tools following the established pattern."""
|
197
245
|
try:
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# src/diagram_to_iac/core/config_loader.py
|
2
|
+
"""
|
3
|
+
Central configuration loader for diagram-to-iac project.
|
4
|
+
Handles loading and merging configuration from multiple sources with environment variable overrides.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import yaml
|
9
|
+
import logging
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Dict, Any, Optional, Union
|
12
|
+
from functools import lru_cache
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
class ConfigLoader:
|
17
|
+
"""
|
18
|
+
Central configuration management for the diagram-to-iac project.
|
19
|
+
Loads configuration from YAML files and provides environment variable override capability.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, app_config_path: Optional[str] = None, system_config_path: Optional[str] = None):
|
23
|
+
"""
|
24
|
+
Initialize ConfigLoader with optional custom config paths.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
app_config_path: Path to application config file (default: src/diagram_to_iac/config.yaml)
|
28
|
+
system_config_path: Path to system config file (default: config/system.yaml)
|
29
|
+
"""
|
30
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
31
|
+
|
32
|
+
# Set default paths
|
33
|
+
self.base_path = Path(__file__).parent.parent.parent.parent # diagram-to-iac root
|
34
|
+
self.app_config_path = Path(app_config_path) if app_config_path else self.base_path / "src" / "diagram_to_iac" / "config.yaml"
|
35
|
+
self.system_config_path = Path(system_config_path) if system_config_path else self.base_path / "config" / "system.yaml"
|
36
|
+
|
37
|
+
# Cache for loaded configs
|
38
|
+
self._app_config = None
|
39
|
+
self._system_config = None
|
40
|
+
self._merged_config = None
|
41
|
+
|
42
|
+
@lru_cache(maxsize=1)
|
43
|
+
def get_config(self) -> Dict[str, Any]:
|
44
|
+
"""
|
45
|
+
Get the complete merged configuration with environment variable overrides.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
Merged configuration dictionary
|
49
|
+
"""
|
50
|
+
if self._merged_config is None:
|
51
|
+
self._merged_config = self._load_and_merge_configs()
|
52
|
+
return self._merged_config
|
53
|
+
|
54
|
+
def _load_and_merge_configs(self) -> Dict[str, Any]:
|
55
|
+
"""
|
56
|
+
Load and merge all configuration sources.
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
Merged configuration dictionary
|
60
|
+
"""
|
61
|
+
# Load base configs
|
62
|
+
app_config = self._load_app_config()
|
63
|
+
system_config = self._load_system_config()
|
64
|
+
|
65
|
+
# Start with system config as base, overlay app config
|
66
|
+
merged = self._deep_merge(system_config, app_config)
|
67
|
+
|
68
|
+
# Apply environment variable overrides
|
69
|
+
merged = self._apply_env_overrides(merged)
|
70
|
+
|
71
|
+
self.logger.debug("Configuration loaded and merged successfully")
|
72
|
+
return merged
|
73
|
+
|
74
|
+
def _load_app_config(self) -> Dict[str, Any]:
|
75
|
+
"""Load application configuration from YAML file."""
|
76
|
+
if self._app_config is None:
|
77
|
+
try:
|
78
|
+
if self.app_config_path.exists():
|
79
|
+
with open(self.app_config_path, 'r') as f:
|
80
|
+
self._app_config = yaml.safe_load(f) or {}
|
81
|
+
self.logger.debug(f"Loaded app config from {self.app_config_path}")
|
82
|
+
else:
|
83
|
+
self.logger.warning(f"App config file not found: {self.app_config_path}")
|
84
|
+
self._app_config = {}
|
85
|
+
except Exception as e:
|
86
|
+
self.logger.error(f"Failed to load app config: {e}")
|
87
|
+
self._app_config = {}
|
88
|
+
return self._app_config
|
89
|
+
|
90
|
+
def _load_system_config(self) -> Dict[str, Any]:
|
91
|
+
"""Load system configuration from YAML file."""
|
92
|
+
if self._system_config is None:
|
93
|
+
try:
|
94
|
+
if self.system_config_path.exists():
|
95
|
+
with open(self.system_config_path, 'r') as f:
|
96
|
+
self._system_config = yaml.safe_load(f) or {}
|
97
|
+
self.logger.debug(f"Loaded system config from {self.system_config_path}")
|
98
|
+
else:
|
99
|
+
self.logger.warning(f"System config file not found: {self.system_config_path}")
|
100
|
+
self._system_config = {}
|
101
|
+
except Exception as e:
|
102
|
+
self.logger.error(f"Failed to load system config: {e}")
|
103
|
+
self._system_config = {}
|
104
|
+
return self._system_config
|
105
|
+
|
106
|
+
def _deep_merge(self, base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]:
|
107
|
+
"""
|
108
|
+
Deep merge two dictionaries, with overlay taking precedence.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
base: Base dictionary
|
112
|
+
overlay: Dictionary to overlay on base
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Merged dictionary
|
116
|
+
"""
|
117
|
+
result = base.copy()
|
118
|
+
|
119
|
+
for key, value in overlay.items():
|
120
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
121
|
+
result[key] = self._deep_merge(result[key], value)
|
122
|
+
else:
|
123
|
+
result[key] = value
|
124
|
+
|
125
|
+
return result
|
126
|
+
|
127
|
+
def _apply_env_overrides(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
128
|
+
"""
|
129
|
+
Apply environment variable overrides to configuration.
|
130
|
+
Environment variables should be in the format: DIAGRAM_TO_IAC_<SECTION>_<KEY>
|
131
|
+
|
132
|
+
Args:
|
133
|
+
config: Base configuration dictionary
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
Configuration with environment overrides applied
|
137
|
+
"""
|
138
|
+
result = config.copy()
|
139
|
+
env_prefix = "DIAGRAM_TO_IAC_"
|
140
|
+
|
141
|
+
# Get list of allowed overrides from config
|
142
|
+
allowed_overrides = config.get("environment_overrides", {}).get("allowed_overrides", [])
|
143
|
+
|
144
|
+
for env_var, env_value in os.environ.items():
|
145
|
+
if not env_var.startswith(env_prefix):
|
146
|
+
continue
|
147
|
+
|
148
|
+
# Parse environment variable name
|
149
|
+
# e.g., DIAGRAM_TO_IAC_NETWORK_API_TIMEOUT -> network.api_timeout
|
150
|
+
var_path = env_var[len(env_prefix):].lower().replace('_', '.')
|
151
|
+
|
152
|
+
# Check if override is allowed
|
153
|
+
if var_path not in allowed_overrides:
|
154
|
+
self.logger.debug(f"Environment override not allowed: {var_path}")
|
155
|
+
continue
|
156
|
+
|
157
|
+
# Convert string value to appropriate type
|
158
|
+
converted_value = self._convert_env_value(env_value)
|
159
|
+
|
160
|
+
# Apply override to config
|
161
|
+
self._set_nested_value(result, var_path, converted_value)
|
162
|
+
self.logger.debug(f"Applied environment override: {var_path} = {converted_value}")
|
163
|
+
|
164
|
+
return result
|
165
|
+
|
166
|
+
def _convert_env_value(self, value: str) -> Union[str, int, float, bool]:
|
167
|
+
"""
|
168
|
+
Convert environment variable string value to appropriate type.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
value: String value from environment variable
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
Converted value
|
175
|
+
"""
|
176
|
+
# Handle boolean values
|
177
|
+
if value.lower() in ("true", "yes", "1", "on"):
|
178
|
+
return True
|
179
|
+
elif value.lower() in ("false", "no", "0", "off"):
|
180
|
+
return False
|
181
|
+
|
182
|
+
# Handle numeric values
|
183
|
+
try:
|
184
|
+
if '.' in value:
|
185
|
+
return float(value)
|
186
|
+
else:
|
187
|
+
return int(value)
|
188
|
+
except ValueError:
|
189
|
+
pass
|
190
|
+
|
191
|
+
# Return as string
|
192
|
+
return value
|
193
|
+
|
194
|
+
def _set_nested_value(self, config: Dict[str, Any], path: str, value: Any) -> None:
|
195
|
+
"""
|
196
|
+
Set a nested value in configuration using dot notation.
|
197
|
+
|
198
|
+
Args:
|
199
|
+
config: Configuration dictionary to modify
|
200
|
+
path: Dot-separated path (e.g., "network.api_timeout")
|
201
|
+
value: Value to set
|
202
|
+
"""
|
203
|
+
keys = path.split('.')
|
204
|
+
current = config
|
205
|
+
|
206
|
+
# Navigate to the parent of the target key
|
207
|
+
for key in keys[:-1]:
|
208
|
+
if key not in current:
|
209
|
+
current[key] = {}
|
210
|
+
current = current[key]
|
211
|
+
|
212
|
+
# Set the final value
|
213
|
+
current[keys[-1]] = value
|
214
|
+
|
215
|
+
def get_section(self, section: str) -> Dict[str, Any]:
|
216
|
+
"""
|
217
|
+
Get a specific configuration section.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
section: Section name (e.g., "network", "ai", "routing")
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
Configuration section dictionary
|
224
|
+
"""
|
225
|
+
return self.get_config().get(section, {})
|
226
|
+
|
227
|
+
def get_value(self, path: str, default: Any = None) -> Any:
|
228
|
+
"""
|
229
|
+
Get a configuration value using dot notation.
|
230
|
+
|
231
|
+
Args:
|
232
|
+
path: Dot-separated path (e.g., "network.api_timeout")
|
233
|
+
default: Default value if path not found
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
Configuration value or default
|
237
|
+
"""
|
238
|
+
keys = path.split('.')
|
239
|
+
current = self.get_config()
|
240
|
+
|
241
|
+
try:
|
242
|
+
for key in keys:
|
243
|
+
current = current[key]
|
244
|
+
return current
|
245
|
+
except (KeyError, TypeError):
|
246
|
+
return default
|
247
|
+
|
248
|
+
def reload(self) -> None:
|
249
|
+
"""Reload configuration from files (clears cache)."""
|
250
|
+
self._app_config = None
|
251
|
+
self._system_config = None
|
252
|
+
self._merged_config = None
|
253
|
+
self.get_config.cache_clear()
|
254
|
+
self.logger.debug("Configuration cache cleared, will reload on next access")
|
255
|
+
|
256
|
+
|
257
|
+
# Global configuration loader instance
|
258
|
+
_config_loader = None
|
259
|
+
|
260
|
+
def get_config_loader() -> ConfigLoader:
|
261
|
+
"""Get the global configuration loader instance."""
|
262
|
+
global _config_loader
|
263
|
+
if _config_loader is None:
|
264
|
+
_config_loader = ConfigLoader()
|
265
|
+
return _config_loader
|
266
|
+
|
267
|
+
def get_config() -> Dict[str, Any]:
|
268
|
+
"""Get the complete merged configuration."""
|
269
|
+
return get_config_loader().get_config()
|
270
|
+
|
271
|
+
def get_config_section(section: str) -> Dict[str, Any]:
|
272
|
+
"""Get a specific configuration section."""
|
273
|
+
return get_config_loader().get_section(section)
|
274
|
+
|
275
|
+
def get_config_value(path: str, default: Any = None) -> Any:
|
276
|
+
"""Get a configuration value using dot notation."""
|
277
|
+
return get_config_loader().get_value(path, default)
|
278
|
+
|
279
|
+
def reload_config() -> None:
|
280
|
+
"""Reload configuration from files."""
|
281
|
+
get_config_loader().reload()
|
diagram_to_iac/core/memory.py
CHANGED
@@ -2,7 +2,7 @@ from typing import Any, Dict, List, Optional
|
|
2
2
|
import json
|
3
3
|
import os
|
4
4
|
from pathlib import Path
|
5
|
-
from subprocess import check_output, CalledProcessError
|
5
|
+
from subprocess import check_output, CalledProcessError, TimeoutExpired
|
6
6
|
# Intended to be a thin wrapper over LangGraph's StateGraph node store.
|
7
7
|
# For now, a simple dictionary-based state.
|
8
8
|
|
@@ -126,7 +126,12 @@ def save_agent_state(state: Dict[str, Any]) -> None:
|
|
126
126
|
def current_git_sha() -> Optional[str]:
|
127
127
|
"""Return the current git commit SHA, or ``None`` if unavailable."""
|
128
128
|
try:
|
129
|
-
return check_output(
|
129
|
+
return check_output(
|
130
|
+
["git", "rev-parse", "HEAD"],
|
131
|
+
text=True,
|
132
|
+
timeout=5,
|
133
|
+
env={**os.environ, "GIT_TERMINAL_PROMPT": "0"}
|
134
|
+
).strip()
|
130
135
|
except Exception:
|
131
136
|
return None
|
132
137
|
|
@@ -7,6 +7,14 @@ import google.generativeai as genai
|
|
7
7
|
import googleapiclient.discovery
|
8
8
|
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
9
9
|
|
10
|
+
# Import centralized configuration
|
11
|
+
try:
|
12
|
+
from diagram_to_iac.core.config_loader import get_config_value
|
13
|
+
except ImportError:
|
14
|
+
# Fallback if config system not available
|
15
|
+
def get_config_value(path: str, default=None):
|
16
|
+
return default
|
17
|
+
|
10
18
|
def test_openai_api():
|
11
19
|
try:
|
12
20
|
if not os.environ.get("OPENAI_API_KEY"):
|
@@ -14,7 +22,10 @@ def test_openai_api():
|
|
14
22
|
return False
|
15
23
|
client = OpenAI()
|
16
24
|
|
17
|
-
#
|
25
|
+
# Get timeout from configuration
|
26
|
+
api_timeout = get_config_value("network.api_timeout", 10)
|
27
|
+
|
28
|
+
# Run the API call with configured timeout
|
18
29
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
19
30
|
future = executor.submit(
|
20
31
|
client.chat.completions.create,
|
@@ -23,9 +34,9 @@ def test_openai_api():
|
|
23
34
|
max_tokens=10
|
24
35
|
)
|
25
36
|
try:
|
26
|
-
response = future.result(timeout=
|
37
|
+
response = future.result(timeout=api_timeout)
|
27
38
|
except TimeoutError:
|
28
|
-
print("❌ OpenAI API error: request timed out.")
|
39
|
+
print(f"❌ OpenAI API error: request timed out after {api_timeout}s.")
|
29
40
|
return False
|
30
41
|
return True
|
31
42
|
except Exception as e:
|
@@ -41,13 +52,16 @@ def test_gemini_api():
|
|
41
52
|
genai.configure(api_key=google_api_key)
|
42
53
|
model = genai.GenerativeModel('gemini-2.0-flash') # Corrected model name
|
43
54
|
|
44
|
-
#
|
55
|
+
# Get timeout from configuration
|
56
|
+
api_timeout = get_config_value("network.api_timeout", 10)
|
57
|
+
|
58
|
+
# Run the API call with configured timeout
|
45
59
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
46
60
|
future = executor.submit(model.generate_content, "Hello, are you working?")
|
47
61
|
try:
|
48
|
-
response = future.result(timeout=
|
62
|
+
response = future.result(timeout=api_timeout)
|
49
63
|
except TimeoutError:
|
50
|
-
print("❌ Gemini API error: request timed out.")
|
64
|
+
print(f"❌ Gemini API error: request timed out after {api_timeout}s.")
|
51
65
|
return False
|
52
66
|
return True
|
53
67
|
except Exception as e:
|
@@ -61,7 +75,10 @@ def test_anthropic_api():
|
|
61
75
|
return False
|
62
76
|
client = Anthropic()
|
63
77
|
|
64
|
-
#
|
78
|
+
# Get timeout from configuration
|
79
|
+
api_timeout = get_config_value("network.api_timeout", 10)
|
80
|
+
|
81
|
+
# Run the API call with configured timeout
|
65
82
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
66
83
|
future = executor.submit(
|
67
84
|
client.messages.create,
|
@@ -70,9 +87,9 @@ def test_anthropic_api():
|
|
70
87
|
messages=[{"role": "user", "content": "Hello, are you working?"}]
|
71
88
|
)
|
72
89
|
try:
|
73
|
-
response = future.result(timeout=
|
90
|
+
response = future.result(timeout=api_timeout)
|
74
91
|
except TimeoutError:
|
75
|
-
print("❌ Anthropic API error: request timed out.")
|
92
|
+
print(f"❌ Anthropic API error: request timed out after {api_timeout}s.")
|
76
93
|
return False
|
77
94
|
return True
|
78
95
|
except Exception as e:
|
@@ -92,7 +109,10 @@ def test_github_api():
|
|
92
109
|
"Accept": "application/vnd.github.v3+json"
|
93
110
|
}
|
94
111
|
|
95
|
-
#
|
112
|
+
# Get timeout from configuration
|
113
|
+
github_timeout = get_config_value("network.github_timeout", 15)
|
114
|
+
|
115
|
+
# Run the API call with configured timeout
|
96
116
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
97
117
|
future = executor.submit(
|
98
118
|
requests.get,
|
@@ -100,9 +120,9 @@ def test_github_api():
|
|
100
120
|
headers=headers
|
101
121
|
)
|
102
122
|
try:
|
103
|
-
response = future.result(timeout=
|
123
|
+
response = future.result(timeout=github_timeout)
|
104
124
|
except TimeoutError:
|
105
|
-
print("❌ GitHub API error: request timed out.")
|
125
|
+
print(f"❌ GitHub API error: request timed out after {github_timeout}s.")
|
106
126
|
return False
|
107
127
|
|
108
128
|
if response.status_code == 200:
|
@@ -119,33 +139,53 @@ def test_github_api():
|
|
119
139
|
|
120
140
|
def test_Terraform_API():
|
121
141
|
try:
|
122
|
-
|
142
|
+
tfe_token = os.environ.get("TFE_TOKEN")
|
143
|
+
if not tfe_token:
|
123
144
|
print("❌ Terraform API error: TFE_TOKEN environment variable not set.")
|
124
145
|
return False
|
146
|
+
|
125
147
|
headers = {
|
126
|
-
"Authorization": f"Bearer {
|
148
|
+
"Authorization": f"Bearer {tfe_token}",
|
127
149
|
"Content-Type": "application/vnd.api+json"
|
128
150
|
}
|
129
|
-
|
151
|
+
|
152
|
+
# Get timeout from configuration
|
153
|
+
terraform_timeout = get_config_value("network.terraform_timeout", 30)
|
154
|
+
|
155
|
+
# Test account details endpoint (more reliable than organizations)
|
156
|
+
# Run the API call with configured timeout
|
130
157
|
with ThreadPoolExecutor(max_workers=1) as executor:
|
131
158
|
future = executor.submit(
|
132
159
|
requests.get,
|
133
|
-
"https://app.terraform.io/api/v2/
|
160
|
+
"https://app.terraform.io/api/v2/account/details",
|
134
161
|
headers=headers
|
135
162
|
)
|
136
163
|
try:
|
137
|
-
response = future.result(timeout=
|
164
|
+
response = future.result(timeout=terraform_timeout)
|
138
165
|
except TimeoutError:
|
139
|
-
print("❌ Terraform API error: request timed out.")
|
166
|
+
print(f"❌ Terraform API error: request timed out after {terraform_timeout}s.")
|
140
167
|
return False
|
168
|
+
|
141
169
|
if response.status_code == 200:
|
142
|
-
|
143
|
-
# print(f"✅ Terraform API works! Organizations: {org_data}")
|
170
|
+
print("✅ Terraform API works! Account access verified.")
|
144
171
|
return True
|
145
172
|
else:
|
146
173
|
print(f"❌ Terraform API error: Status code {response.status_code}")
|
147
174
|
if hasattr(response, 'text'):
|
148
175
|
print(f" Response body: {response.text}")
|
176
|
+
|
177
|
+
# Provide specific guidance for common error codes
|
178
|
+
if response.status_code == 401:
|
179
|
+
print(" This is an authentication error. Common causes:")
|
180
|
+
print(" - TFE_TOKEN is invalid or expired")
|
181
|
+
print(" - Token format is incorrect (should be a Terraform Cloud API token)")
|
182
|
+
print(" - Token doesn't have sufficient permissions")
|
183
|
+
print(" - Please verify your token at: https://app.terraform.io/app/settings/tokens")
|
184
|
+
elif response.status_code == 403:
|
185
|
+
print(" This is a permission error. The token is valid but lacks access rights.")
|
186
|
+
elif response.status_code == 404:
|
187
|
+
print(" The API endpoint was not found. Check if the URL is correct.")
|
188
|
+
|
149
189
|
return False
|
150
190
|
except requests.exceptions.ConnectionError as e:
|
151
191
|
print(f"❌ Terraform API error: Connection failed - {str(e)}")
|
@@ -167,6 +207,7 @@ def test_Terraform_API():
|
|
167
207
|
print(f"❌ Terraform API error: Unexpected error - {str(e)}")
|
168
208
|
return False
|
169
209
|
|
210
|
+
|
170
211
|
def test_all_apis():
|
171
212
|
print("Hello from the test workflow!")
|
172
213
|
|