From 3906e9f52c999047865962b2906ee74c340939d3 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Wed, 4 Feb 2026 21:54:19 +0000 Subject: [PATCH] Add source code files --- config_converter/cli.py | 340 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 config_converter/cli.py diff --git a/config_converter/cli.py b/config_converter/cli.py new file mode 100644 index 0000000..ef3ed81 --- /dev/null +++ b/config_converter/cli.py @@ -0,0 +1,340 @@ +"""CLI interface for Config Converter.""" + +import glob +import os +import sys +from pathlib import Path +from typing import List, Optional + +import click + +from config_converter.converters import BaseConverter, JsonConverter, YamlConverter, TomlConverter, IniConverter +from config_converter.generators import TypeScriptGenerator +from config_converter.validators import SchemaInferrer, SchemaValidator +from config_converter.utils import OutputFormatter + + +@click.group() +@click.option("--color/--no-color", default=True, help="Enable/disable colorized output") +@click.pass_context +def main(ctx: click.Context, color: bool) -> None: + """Config Converter CLI - Convert configuration files between formats.""" + ctx.ensure_object(dict) + ctx.obj["color"] = color + ctx.obj["formatter"] = OutputFormatter(use_color=color) + + +@main.command("convert") +@click.argument("source", type=click.Path(exists=True)) +@click.option("--from", "-f", "from_format", type=str, help="Source format (auto-detected if not specified)") +@click.option("--to", "-t", "to_format", required=True, type=click.Choice(["json", "yaml", "toml", "ini"]), help="Target format") +@click.option("--output", "-o", "output_path", type=click.Path(), help="Output file path (stdout if not specified)") +@click.option("--overwrite", "-w", is_flag=True, help="Overwrite output file if it exists") +@click.pass_context +def convert( + ctx: click.Context, + source: str, + from_format: Optional[str], + to_format: str, + output_path: Optional[str], + overwrite: bool, +) -> None: + """Convert a configuration file from one format to another.""" + formatter: OutputFormatter = ctx.obj["formatter"] + + source_path = Path(source) + + if from_format is None: + from_format = BaseConverter.guess_format(source_path) + if from_format is None: + formatter.print_error(f"Could not detect format from extension: {source_path.suffix}") + sys.exit(1) + + try: + source_converter = BaseConverter.get_converter(from_format) + target_converter = BaseConverter.get_converter(to_format) + except Exception as e: + formatter.print_error(str(e)) + sys.exit(1) + + try: + data = source_converter.read(source_path) + except Exception as e: + formatter.print_error(f"Failed to read {source}: {e}") + sys.exit(1) + + if output_path: + output_file = Path(output_path) + if output_file.exists() and not overwrite: + formatter.print_error(f"Output file already exists: {output_path}") + sys.exit(1) + try: + target_converter.write(data, output_file) + formatter.print_success(f"Converted {source} ({from_format}) → {output_path} ({to_format})") + except Exception as e: + formatter.print_error(f"Failed to write {output_path}: {e}") + sys.exit(1) + else: + try: + output = target_converter.format(data) + click.echo(output) + except Exception as e: + formatter.print_error(f"Failed to format output: {e}") + sys.exit(1) + + +@main.command("batch") +@click.argument("pattern", type=str) +@click.option("--from", "-f", "from_format", type=str, help="Source format (auto-detected if not specified)") +@click.option("--to", "-t", "to_format", required=True, type=click.Choice(["json", "yaml", "toml", "ini"]), help="Target format") +@click.option("--output-dir", "-o", "output_dir", type=click.Path(), help="Output directory for converted files") +@click.option("--overwrite", "-w", is_flag=True, help="Overwrite output files if they exist") +@click.pass_context +def batch_convert( + ctx: click.Context, + pattern: str, + from_format: Optional[str], + to_format: str, + output_dir: Optional[str], + overwrite: bool, +) -> None: + """Convert multiple files matching a glob pattern.""" + formatter: OutputFormatter = ctx.obj["formatter"] + + files = glob.glob(pattern) + if not files: + formatter.print_error(f"No files found matching pattern: {pattern}") + sys.exit(1) + + try: + target_converter = BaseConverter.get_converter(to_format) + except Exception as e: + formatter.print_error(str(e)) + sys.exit(1) + + if output_dir: + Path(output_dir).mkdir(parents=True, exist_ok=True) + + success_count = 0 + failed_count = 0 + failed_files: List[str] = [] + + for file_path in files: + source_path = Path(file_path) + + try: + if from_format is None: + fmt = BaseConverter.guess_format(source_path) + if fmt is None: + failed_count += 1 + failed_files.append(file_path) + continue + else: + fmt = from_format + + converter = BaseConverter.get_converter(fmt) + data = converter.read(source_path) + + if output_dir: + output_path = Path(output_dir) / f"{source_path.stem}.{to_format}" + if output_path.exists() and not overwrite: + failed_count += 1 + failed_files.append(file_path) + continue + converter.write(data, output_path) + else: + formatter.print_error(f"Output directory required for batch conversion") + failed_count += 1 + failed_files.append(file_path) + continue + + success_count += 1 + + except Exception as e: + failed_count += 1 + failed_files.append(file_path) + + formatter.print_info(f"Batch conversion complete: {success_count} succeeded, {failed_count} failed") + + if failed_files: + formatter.print_warning("Failed files:") + for f in failed_files: + formatter.print_warning(f" - {f}") + + +@main.command("infer") +@click.argument("source", type=click.Path(exists=True)) +@click.option("--format", "-f", "fmt", type=str, help="Input format (auto-detected if not specified)") +@click.option("--table", "-t", is_flag=True, help="Display as table") +@click.option("--tree", is_flag=True, help="Display as tree") +@click.pass_context +def infer_schema( + ctx: click.Context, + source: str, + fmt: Optional[str], + table: bool, + tree: bool, +) -> None: + """Infer schema from a configuration file.""" + formatter: OutputFormatter = ctx.obj["formatter"] + source_path = Path(source) + + if fmt is None: + fmt = BaseConverter.guess_format(source_path) + if fmt is None: + formatter.print_error(f"Could not detect format from extension: {source_path.suffix}") + sys.exit(1) + + try: + converter = BaseConverter.get_converter(fmt) + data = converter.read(source_path) + except Exception as e: + formatter.print_error(f"Failed to read {source}: {e}") + sys.exit(1) + + inferrer = SchemaInferrer() + schema = inferrer.infer(data) + schema_dict = schema.to_dict() + + if table: + formatter.print_schema_table(schema_dict) + elif tree: + formatter.print_schema_tree(schema_dict) + else: + import json + click.echo(json.dumps(schema_dict, indent=2)) + + +@main.command("validate") +@click.argument("source", type=click.Path(exists=True)) +@click.option("--format", "-f", "fmt", type=str, help="Input format (auto-detected if not specified)") +@click.pass_context +def validate( + ctx: click.Context, + source: str, + fmt: Optional[str], +) -> None: + """Validate a configuration file against its inferred schema.""" + formatter: OutputFormatter = ctx.obj["formatter"] + source_path = Path(source) + + if fmt is None: + fmt = BaseConverter.guess_format(source_path) + if fmt is None: + formatter.print_error(f"Could not detect format from extension: {source_path.suffix}") + sys.exit(1) + + try: + converter = BaseConverter.get_converter(fmt) + data = converter.read(source_path) + except Exception as e: + formatter.print_error(f"Failed to read {source}: {e}") + sys.exit(1) + + inferrer = SchemaInferrer() + schema = inferrer.infer(data) + + validator = SchemaValidator(schema) + is_valid, errors = validator.validate(data) + + if is_valid: + formatter.print_success(f"Validation passed: {source}") + else: + formatter.print_error(f"Validation failed: {source}") + for error in errors: + formatter.print_error(f" - {error}") + sys.exit(1) + + +@main.command("generate-ts") +@click.argument("source", type=click.Path(exists=True)) +@click.option("--format", "-f", "fmt", type=str, help="Input format (auto-detected if not specified)") +@click.option("--interface", "-i", "interface_name", default="Config", help="Name for the generated interface") +@click.option("--output", "-o", "output_path", type=click.Path(), help="Output file path (stdout if not specified)") +@click.pass_context +def generate_typescript( + ctx: click.Context, + source: str, + fmt: Optional[str], + interface_name: str, + output_path: Optional[str], +) -> None: + """Generate TypeScript interfaces from a configuration file.""" + formatter: OutputFormatter = ctx.obj["formatter"] + source_path = Path(source) + + if fmt is None: + fmt = BaseConverter.guess_format(source_path) + if fmt is None: + formatter.print_error(f"Could not detect format from extension: {source_path.suffix}") + sys.exit(1) + + try: + converter = BaseConverter.get_converter(fmt) + data = converter.read(source_path) + except Exception as e: + formatter.print_error(f"Failed to read {source}: {e}") + sys.exit(1) + + generator = TypeScriptGenerator(interface_name=interface_name) + ts_output = generator.generate(data) + + if output_path: + try: + with open(output_path, "w", encoding="utf-8") as f: + f.write(ts_output) + formatter.print_success(f"Generated TypeScript interfaces: {output_path}") + except Exception as e: + formatter.print_error(f"Failed to write {output_path}: {e}") + sys.exit(1) + else: + click.echo(ts_output) + + +@main.command("interactive") +@click.pass_context +def interactive(ctx: click.Context) -> None: + """Enter interactive mode for guided conversion.""" + formatter: OutputFormatter = ctx.obj["formatter"] + + formatter.print_title("Config Converter - Interactive Mode") + + supported_formats = BaseConverter.get_supported_formats() + formatter.print_info(f"Supported formats: {', '.join(supported_formats)}") + + source = click.prompt("Source file path", type=click.Path(exists=True)) + source_format = click.prompt("Source format (or press Enter for auto-detect)", type=str, default="", show_default=False) + + if not source_format: + source_format = BaseConverter.guess_format(source) + if source_format is None: + formatter.print_error("Could not detect format. Please specify manually.") + return + + target_format = click.prompt("Target format", type=click.Choice(supported_formats)) + + output_path = click.prompt("Output file path (or press Enter for stdout)", type=str, default="", show_default=False) + + ctx.invoke( + convert, + source=source, + from_format=source_format, + to_format=target_format, + output_path=output_path if output_path else None, + overwrite=True, + ) + + +@main.command("formats") +@click.pass_context +def list_formats(ctx: click.Context) -> None: + """List all supported formats.""" + formatter: OutputFormatter = ctx.obj["formatter"] + supported = BaseConverter.get_supported_formats() + formatter.print_info("Supported formats:") + for fmt in supported: + formatter.print_info(f" - {fmt}") + + +if __name__ == "__main__": + main()