Initial upload with full project structure

This commit is contained in:
2026-02-01 20:49:02 +00:00
parent 492e3b2d38
commit 94529b9921

276
app/src/confgen/editor.py Normal file
View 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"