""" Modelgen - Generic Model Generation Tool Generates typed models from various sources to various formats. Input sources: - from-config: Configuration files (soleprint config.json style) - from-schema: Python dataclasses in schema/ folder - extract: Existing codebases (Django, SQLAlchemy, Prisma) Output formats: - pydantic: Pydantic BaseModel classes - django: Django ORM models - typescript: TypeScript interfaces - protobuf: Protocol Buffer definitions - prisma: Prisma schema Usage: python -m soleprint.station.tools.modelgen --help python -m soleprint.station.tools.modelgen from-config -c config.json -o models.py python -m soleprint.station.tools.modelgen from-schema -o models/ --targets pydantic,typescript python -m soleprint.station.tools.modelgen extract --source /path/to/django --targets pydantic python -m soleprint.station.tools.modelgen generate --config schema/modelgen.json """ import argparse import sys from pathlib import Path from .generator import GENERATORS def cmd_from_config(args): """Generate models from a configuration file (soleprint config.json style).""" from .loader import load_config from .model_generator import ModelGenerator config_path = Path(args.config) if not config_path.exists(): print(f"Error: Config file not found: {config_path}", file=sys.stderr) sys.exit(1) output_path = Path(args.output) print(f"Loading config: {config_path}") config = load_config(config_path) print(f"Generating {args.format} models to: {output_path}") generator = ModelGenerator( config=config, output_path=output_path, output_format=args.format, ) result_path = generator.generate() print(f"Models generated: {result_path}") def cmd_from_schema(args): """Generate models from Python dataclasses in schema/ folder.""" from .loader import load_schema from .writer import write_file # Determine schema path schema_path = Path(args.schema) if args.schema else Path.cwd() / "schema" if not schema_path.exists(): print(f"Error: Schema folder not found: {schema_path}", file=sys.stderr) print( "Create a schema/ folder with Python dataclasses and an __init__.py", file=sys.stderr, ) print("that exports DATACLASSES and ENUMS lists.", file=sys.stderr) sys.exit(1) # Parse include groups include = None if args.include: include = {g.strip() for g in args.include.split(",")} print(f"Loading schema: {schema_path}") schema = load_schema(schema_path, include=include) loaded = [] if schema.models: loaded.append(f"{len(schema.models)} models") if schema.enums: loaded.append(f"{len(schema.enums)} enums") if schema.api_models: loaded.append(f"{len(schema.api_models)} api models") if schema.grpc_messages: loaded.append(f"{len(schema.grpc_messages)} grpc messages") print(f"Found {', '.join(loaded)}") # Parse targets targets = [t.strip() for t in args.targets.split(",")] output_dir = Path(args.output) for target in targets: if target not in GENERATORS: print(f"Warning: Unknown target '{target}', skipping", file=sys.stderr) continue generator = GENERATORS[target]() ext = generator.file_extension() # Determine output filename (use target name to avoid overwrites) if len(targets) == 1 and args.output.endswith(ext): output_file = output_dir else: output_file = output_dir / f"models_{target}{ext}" print(f"Generating {target} to: {output_file}") generator.generate(schema, output_file) print("Done!") def cmd_extract(args): """Extract models from existing codebase.""" from .loader.extract import EXTRACTORS source_path = Path(args.source) if not source_path.exists(): print(f"Error: Source path not found: {source_path}", file=sys.stderr) sys.exit(1) # Auto-detect or use specified framework framework = args.framework extractor = None if framework == "auto": for name, extractor_cls in EXTRACTORS.items(): ext = extractor_cls(source_path) if ext.detect(): framework = name extractor = ext print(f"Detected framework: {framework}") break if not extractor: print("Error: Could not auto-detect framework", file=sys.stderr) print(f"Available frameworks: {list(EXTRACTORS.keys())}", file=sys.stderr) sys.exit(1) else: if framework not in EXTRACTORS: print(f"Error: Unknown framework: {framework}", file=sys.stderr) print(f"Available: {list(EXTRACTORS.keys())}", file=sys.stderr) sys.exit(1) extractor = EXTRACTORS[framework](source_path) print(f"Extracting from: {source_path}") models, enums = extractor.extract() print(f"Extracted {len(models)} models, {len(enums)} enums") # Parse targets targets = [t.strip() for t in args.targets.split(",")] output_dir = Path(args.output) for target in targets: if target not in GENERATORS: print(f"Warning: Unknown target '{target}', skipping", file=sys.stderr) continue generator = GENERATORS[target]() ext = generator.file_extension() # Determine output filename (use target name to avoid overwrites) if len(targets) == 1 and args.output.endswith(ext): output_file = output_dir else: output_file = output_dir / f"models_{target}{ext}" print(f"Generating {target} to: {output_file}") generator.generate((models, enums), output_file) print("Done!") def cmd_generate(args): """Generate all targets from a JSON config file.""" import json from .loader import load_schema config_path = Path(args.config) if not config_path.exists(): print(f"Error: Config file not found: {config_path}", file=sys.stderr) sys.exit(1) with open(config_path) as f: config = json.load(f) # Resolve paths relative to current working directory schema_path = Path(config["schema"]) if not schema_path.exists(): print(f"Error: Schema folder not found: {schema_path}", file=sys.stderr) sys.exit(1) print(f"Loading schema: {schema_path}") for target_conf in config["targets"]: target = target_conf["target"] output = Path(target_conf["output"]) include = set(target_conf.get("include", [])) name_map = target_conf.get("name_map", {}) if target not in GENERATORS: print(f"Warning: Unknown target '{target}', skipping", file=sys.stderr) continue # Load schema with this target's include filter schema = load_schema(schema_path, include=include or None) generator = GENERATORS[target](name_map=name_map) print(f"Generating {target} to: {output}") generator.generate(schema, output) print("Done!") def cmd_list_formats(args): """List available output formats.""" print("Available output formats:") for fmt in GENERATORS.keys(): print(f" - {fmt}") def main(): parser = argparse.ArgumentParser( description="Modelgen - Generic Model Generation Tool", formatter_class=argparse.RawDescriptionHelpFormatter, ) subparsers = parser.add_subparsers(dest="command", required=True) # Available formats for help text formats = list(GENERATORS.keys()) formats_str = ", ".join(formats) # from-config command config_parser = subparsers.add_parser( "from-config", help="Generate models from soleprint configuration file", ) config_parser.add_argument( "--config", "-c", type=str, required=True, help="Path to configuration file (e.g., config.json)", ) config_parser.add_argument( "--output", "-o", type=str, required=True, help="Output path (file or directory)", ) config_parser.add_argument( "--format", "-f", type=str, default="pydantic", choices=["pydantic"], # Only pydantic for config mode help="Output format (default: pydantic)", ) config_parser.set_defaults(func=cmd_from_config) # from-schema command schema_parser = subparsers.add_parser( "from-schema", help="Generate models from Python dataclasses in schema/ folder", ) schema_parser.add_argument( "--schema", "-s", type=str, default=None, help="Path to schema folder (default: ./schema)", ) schema_parser.add_argument( "--output", "-o", type=str, required=True, help="Output path (file or directory)", ) schema_parser.add_argument( "--targets", "-t", type=str, default="pydantic", help=f"Comma-separated output targets ({formats_str})", ) schema_parser.add_argument( "--include", type=str, default=None, help="Comma-separated model groups to include (dataclasses,enums,api,grpc). Default: all.", ) schema_parser.set_defaults(func=cmd_from_schema) # extract command extract_parser = subparsers.add_parser( "extract", help="Extract models from existing codebase", ) extract_parser.add_argument( "--source", "-s", type=str, required=True, help="Path to source codebase", ) extract_parser.add_argument( "--framework", "-f", type=str, choices=["django", "sqlalchemy", "prisma", "auto"], default="auto", help="Source framework (default: auto-detect)", ) extract_parser.add_argument( "--output", "-o", type=str, required=True, help="Output path (file or directory)", ) extract_parser.add_argument( "--targets", "-t", type=str, default="pydantic", help=f"Comma-separated output targets ({formats_str})", ) extract_parser.set_defaults(func=cmd_extract) # generate command (config-driven multi-target) gen_parser = subparsers.add_parser( "generate", help="Generate all targets from a JSON config file", ) gen_parser.add_argument( "--config", "-c", type=str, required=True, help="Path to generation config file (e.g., schema/modelgen.json)", ) gen_parser.set_defaults(func=cmd_generate) # list-formats command formats_parser = subparsers.add_parser( "list-formats", help="List available output formats", ) formats_parser.set_defaults(func=cmd_list_formats) args = parser.parse_args() args.func(args) if __name__ == "__main__": main()