Initial upload with full project structure
This commit is contained in:
276
app/src/confgen/editor.py
Normal file
276
app/src/confgen/editor.py
Normal file
@@ -0,0 +1,276 @@
|
||||
"""Interactive TUI editor using prompt_toolkit."""
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def console_print(text: str) -> None:
|
||||
"""Print text to console."""
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
console.print(text)
|
||||
|
||||
|
||||
def confirm(message: str) -> bool:
|
||||
"""Show a confirmation prompt."""
|
||||
from prompt_toolkit import PromptSession
|
||||
|
||||
session = PromptSession()
|
||||
result = session.prompt(f"{message} [y/N]: ")
|
||||
return result.lower() in ("y", "yes")
|
||||
|
||||
|
||||
def get_prompt_for_var(var_name: str, current_value: str) -> str:
|
||||
"""Get the prompt text for a variable."""
|
||||
if is_secret_variable(var_name):
|
||||
return f"[secret] {var_name}"
|
||||
elif is_boolean_variable(var_name, current_value):
|
||||
return f"[boolean] {var_name} (yes/no)"
|
||||
elif is_numeric_variable(var_name, current_value):
|
||||
return f"[number] {var_name}"
|
||||
else:
|
||||
return var_name
|
||||
|
||||
|
||||
def is_secret_variable(var_name: str) -> bool:
|
||||
"""Check if a variable name suggests a secret."""
|
||||
secret_patterns = [
|
||||
"SECRET",
|
||||
"PASSWORD",
|
||||
"API_KEY",
|
||||
"API_SECRET",
|
||||
"TOKEN",
|
||||
"CREDENTIAL",
|
||||
"PRIVATE_KEY",
|
||||
"AUTH",
|
||||
"DB_PASSWORD",
|
||||
]
|
||||
return any(pattern in var_name.upper() for pattern in secret_patterns)
|
||||
|
||||
|
||||
def is_boolean_variable(var_name: str, current_value: str) -> bool:
|
||||
"""Check if a variable is likely a boolean."""
|
||||
bool_names = ["debug", "enabled", "disabled", "verbose", "skip", "force"]
|
||||
if var_name.lower() in bool_names:
|
||||
return True
|
||||
if current_value and current_value.lower() in ("true", "false", "yes", "no", "0", "1"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_numeric_variable(var_name: str, current_value: str) -> bool:
|
||||
"""Check if a variable is likely numeric."""
|
||||
num_names = ["port", "timeout", "retries", "count", "size", "limit"]
|
||||
if var_name.lower() in num_names:
|
||||
return True
|
||||
if "_port" in var_name.lower() or var_name.lower().endswith("port"):
|
||||
return True
|
||||
if current_value:
|
||||
try:
|
||||
float(current_value)
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
class InteractiveEditor:
|
||||
"""Interactive terminal UI for editing config values."""
|
||||
|
||||
def __init__(self, core, template_name: str, environment: str):
|
||||
self.core = core
|
||||
self.template_name = template_name
|
||||
self.environment = environment
|
||||
self.variables = {}
|
||||
self._load_template_info()
|
||||
|
||||
def _load_template_info(self) -> None:
|
||||
"""Load template and environment information."""
|
||||
tmpl = self.core.get_template(self.template_name)
|
||||
if not tmpl:
|
||||
raise ValueError(f"Template '{self.template_name}' not found")
|
||||
|
||||
env_config = self.core.get_environment(self.environment)
|
||||
if env_config:
|
||||
self.variables = env_config.variables.copy()
|
||||
|
||||
from .template import TemplateEngine
|
||||
|
||||
engine = TemplateEngine()
|
||||
if tmpl.path.exists():
|
||||
content = tmpl.path.read_text()
|
||||
extracted = engine.extract_variables(content)
|
||||
|
||||
for var in extracted:
|
||||
if var not in self.variables:
|
||||
self.variables[var] = ""
|
||||
|
||||
def run(self) -> Optional[str]:
|
||||
"""Run the interactive editor."""
|
||||
from prompt_toolkit import PromptSession
|
||||
|
||||
session = PromptSession()
|
||||
|
||||
console_print("\n[bold]Interactive Configuration Editor[/bold]")
|
||||
console_print(f"Template: {self.template_name}")
|
||||
console_print(f"Environment: {self.environment}\n")
|
||||
|
||||
for var_name, current_value in self.variables.items():
|
||||
prompt = get_prompt_for_var(var_name, current_value)
|
||||
|
||||
if is_secret_variable(var_name):
|
||||
value = session.prompt(
|
||||
f"{prompt}: ",
|
||||
is_password=True,
|
||||
)
|
||||
else:
|
||||
value = session.prompt(
|
||||
f"{prompt}: ",
|
||||
default=str(current_value) if current_value else "",
|
||||
)
|
||||
|
||||
if value:
|
||||
self.variables[var_name] = value
|
||||
|
||||
console_print("\n[bold]Generated Configuration:[/bold]\n")
|
||||
|
||||
from .template import TemplateEngine
|
||||
from .parsers import ConfigParser
|
||||
|
||||
engine = TemplateEngine()
|
||||
tmpl = self.core.get_template(self.template_name)
|
||||
|
||||
if tmpl and tmpl.path.exists():
|
||||
content = tmpl.path.read_text()
|
||||
rendered = engine.render(content, self.variables)
|
||||
|
||||
parser = ConfigParser()
|
||||
parsed = parser.parse(rendered)
|
||||
|
||||
from .formatter import OutputFormatter
|
||||
|
||||
formatter = OutputFormatter()
|
||||
formatted = formatter.format_dict(parsed)
|
||||
|
||||
console_print(formatted)
|
||||
|
||||
if confirm("\nSave this configuration?"):
|
||||
return parser.to_yaml(parsed)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class ConfigEditor:
|
||||
"""Advanced config editor with type validation."""
|
||||
|
||||
def __init__(self):
|
||||
self.values = {}
|
||||
|
||||
def prompt_for_value(
|
||||
self,
|
||||
name: str,
|
||||
current_value: Any,
|
||||
vtype: str = "string",
|
||||
required: bool = False,
|
||||
choices: Optional[list] = None,
|
||||
) -> Any:
|
||||
"""Prompt for a value with type validation."""
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.validation import ValidationError, Validator
|
||||
|
||||
session = PromptSession()
|
||||
|
||||
class TypeValidator(Validator):
|
||||
def validate(self, document):
|
||||
text = document.text
|
||||
if required and not text:
|
||||
return
|
||||
|
||||
if vtype == "integer" and text and not text.isdigit():
|
||||
raise ValidationError(message="Must be a valid integer")
|
||||
|
||||
if vtype == "float" and text:
|
||||
try:
|
||||
float(text)
|
||||
except ValueError:
|
||||
raise ValidationError(message="Must be a valid number")
|
||||
|
||||
if vtype == "boolean" and text and text.lower() not in (
|
||||
"true",
|
||||
"false",
|
||||
"yes",
|
||||
"no",
|
||||
):
|
||||
raise ValidationError(
|
||||
message="Must be true/false or yes/no"
|
||||
)
|
||||
|
||||
if vtype == "port" and text:
|
||||
if not text.isdigit() or not (1 <= int(text) <= 65535):
|
||||
raise ValidationError(message="Port must be 1-65535")
|
||||
|
||||
if choices and text and text not in choices:
|
||||
raise ValidationError(
|
||||
message=f"Must be one of: {', '.join(choices)}"
|
||||
)
|
||||
|
||||
prompt_text = get_prompt_for_var(name, str(current_value) if current_value else "")
|
||||
default = str(current_value) if current_value else ""
|
||||
|
||||
validator = TypeValidator()
|
||||
|
||||
while True:
|
||||
try:
|
||||
value = session.prompt(
|
||||
f"{prompt_text}: ",
|
||||
default=default,
|
||||
validator=validator,
|
||||
complete_while_typing=False,
|
||||
)
|
||||
|
||||
if vtype == "boolean":
|
||||
return value.lower() in ("true", "yes", "1")
|
||||
elif vtype == "integer":
|
||||
return int(value) if value else 0
|
||||
elif vtype == "float":
|
||||
return float(value) if value else 0.0
|
||||
else:
|
||||
return value
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def edit_config(
|
||||
self,
|
||||
config: dict[str, Any],
|
||||
schema: Optional[dict] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Edit a configuration dictionary."""
|
||||
for key, value in config.items():
|
||||
if isinstance(value, dict):
|
||||
console_print(f"\n[bold]{key}:[/bold]")
|
||||
schema_props = schema.get("properties", {}) if schema else {}
|
||||
config[key] = self.edit_config(value, schema_props.get(key))
|
||||
else:
|
||||
vtype = self._get_type_from_schema(key, schema) if schema else "string"
|
||||
config[key] = self.prompt_for_value(key, value, vtype)
|
||||
return config
|
||||
|
||||
def _get_type_from_schema(self, key: str, schema: Optional[dict]) -> str:
|
||||
"""Get the expected type from a JSON schema."""
|
||||
if not schema or "properties" not in schema:
|
||||
return "string"
|
||||
|
||||
prop = schema["properties"].get(key, {})
|
||||
prop_type = prop.get("type", "string")
|
||||
|
||||
if prop_type == "integer":
|
||||
return "integer"
|
||||
elif prop_type == "number":
|
||||
return "float"
|
||||
elif prop_type == "boolean":
|
||||
return "boolean"
|
||||
elif prop_type == "object":
|
||||
return "string"
|
||||
else:
|
||||
return "string"
|
||||
Reference in New Issue
Block a user