geronimo.cli.main

Main CLI entrypoint for Geronimo.

This module provides the primary CLI interface using Typer.

  1"""Main CLI entrypoint for Geronimo.
  2
  3This module provides the primary CLI interface using Typer.
  4"""
  5
  6import typer
  7from rich.console import Console
  8from rich.panel import Panel
  9
 10from geronimo import __version__
 11from geronimo.constants import MODULES
 12
 13# Create the main Typer app
 14app = typer.Typer(
 15    name="geronimo",
 16    help="MLOps deployment platform - automate ML model deployments to AWS",
 17    add_completion=False,
 18    no_args_is_help=True,
 19    rich_markup_mode="rich",
 20)
 21
 22# Console for rich output
 23console = Console()
 24
 25
 26def version_callback(value: bool) -> None:
 27    """Print version and exit."""
 28    if value:
 29        console.print(
 30            Panel.fit(
 31                f"[bold blue]Geronimo[/bold blue] v{__version__}\n"
 32                "[dim]MLOps Deployment Platform[/dim]",
 33                border_style="blue",
 34            )
 35        )
 36        raise typer.Exit()
 37
 38
 39@app.callback()
 40def main(
 41    version: bool = typer.Option(
 42        None,
 43        "--version",
 44        "-v",
 45        callback=version_callback,
 46        is_eager=True,
 47        help="Show version and exit.",
 48    ),
 49) -> None:
 50    """Geronimo - Automate ML model deployments to AWS.
 51
 52    Generate production-ready Terraform, Docker, and CI/CD pipelines
 53    for your ML models with industry best practices built-in.
 54    """
 55    pass
 56
 57
 58# ============================================================================
 59# INIT Command
 60# ============================================================================
 61
 62
 63@app.command()
 64def init(
 65    name: str = typer.Option(
 66        None,
 67        "--name",
 68        "-n",
 69        prompt="Project name",
 70        help="Name of the ML project.",
 71    ),
 72    framework: str = typer.Option(
 73        "sklearn",
 74        "--framework",
 75        "-f",
 76        help="ML framework (sklearn, pytorch, tensorflow).",
 77    ),
 78    template: str = typer.Option(
 79        "realtime",
 80        "--template",
 81        "-t",
 82        help="Project type: 'realtime' (API endpoints), 'batch' (pipelines), or 'both'.",
 83    ),
 84    output_dir: str = typer.Option(
 85        ".",
 86        "--output",
 87        "-o",
 88        help="Output directory for the project.",
 89    ),
 90) -> None:
 91    """Initialize a new ML deployment project.
 92
 93    Scaffolds a complete ML project with Geronimo SDK:
 94
 95    Templates:
 96    - realtime: FastAPI endpoints with Endpoint class
 97    - batch: Metaflow pipelines with BatchPipeline class
 98    - both: Combined real-time and batch support
 99    """
100    from geronimo.generators.project import ProjectGenerator
101
102    # Validate template
103    valid_templates = {"realtime", "batch", "both"}
104    if template not in valid_templates:
105        console.print(f"[bold red]Error:[/bold red] Invalid template '{template}'. Choose from: {valid_templates}")
106        raise typer.Exit(code=1)
107
108    console.print(f"\n[bold blue]Initializing project:[/bold blue] {name}")
109    console.print(f"  Template: [cyan]{template}[/cyan]")
110
111    generator = ProjectGenerator(
112        project_name=name,
113        framework=framework,
114        output_dir=output_dir,
115        template=template,
116    )
117
118    try:
119        generator.generate()
120
121        # SDK scaffolding is now handled by ProjectGenerator.generate()
122        # which creates sdk/endpoint.py, sdk/pipeline.py, app.py, flow.py, etc.
123
124        next_steps = [
125            f"cd {name}",
126            "uv sync",
127        ]
128        if template in ("realtime", "both"):
129            next_steps.append(f"uvicorn {name.replace('-', '_')}.app:app --reload  # Run API server")
130        if template in ("batch", "both"):
131            next_steps.append(f"python -m {name.replace('-', '_')}.flow run  # Run batch pipeline")
132
133        console.print(
134            Panel.fit(
135                f"[bold green]✓ Project '{name}' created successfully![/bold green]\n\n"
136                f"Template: [cyan]{template}[/cyan]\n\n"
137                f"Next steps:\n" + "\n".join(f"  {i+1}. {step}" for i, step in enumerate(next_steps)),
138                border_style="green",
139            )
140        )
141    except Exception as e:
142        console.print(f"[bold red]Error:[/bold red] {e}")
143        raise typer.Exit(code=1)
144
145
146def _generate_sdk_scaffold(name: str, output_dir: str, template: str) -> None:
147    """Generate Geronimo SDK scaffold files."""
148    from pathlib import Path
149
150    project_path = Path(output_dir) / name
151    sdk_dir = project_path / "src" / name.replace("-", "_") / "sdk"
152    sdk_dir.mkdir(parents=True, exist_ok=True)
153
154    # SDK __init__.py
155    (sdk_dir / "__init__.py").write_text('"""Geronimo SDK components."""\n')
156
157    # Features file
158    (sdk_dir / "features.py").write_text('''"""Feature definitions for the model."""
159
160from geronimo.features import FeatureSet, Feature
161# from sklearn.preprocessing import StandardScaler, OneHotEncoder
162
163
164class ProjectFeatures(FeatureSet):
165    """Define your features here.
166
167    Example:
168        age = Feature(dtype='numeric', transformer=StandardScaler())
169        category = Feature(dtype='categorical', encoder=OneHotEncoder())
170    """
171
172    pass
173''')
174
175    # Data sources file
176    (sdk_dir / "data_sources.py").write_text('''"""Data source definitions."""
177
178from geronimo.data_sources import DataSource, Query
179
180# Example query-based source:
181# training_data = DataSource(
182#     name="training",
183#     source="snowflake",
184#     query=Query.from_file("queries/train.sql"),
185# )
186
187# Example file-based source:
188# local_data = DataSource(name="local", source="file", path="data/train.csv")
189''')
190
191    # Model file
192    (sdk_dir / "model.py").write_text(f'''"""Model definition."""
193
194from geronimo.models import Model, HyperParams
195
196# from .features import ProjectFeatures
197
198
199class ProjectModel(Model):
200    """Main model class."""
201
202    name = "{name}"
203    version = "1.0.0"
204    # features = ProjectFeatures()
205
206    def train(self, X, y, params: HyperParams) -> None:
207        """Train the model."""
208        # self.estimator = YourModel(**params.to_dict())
209        # self.estimator.fit(X, y)
210        raise NotImplementedError("Implement train() method")
211
212    def predict(self, X):
213        """Generate predictions."""
214        # return self.estimator.predict(X)
215        raise NotImplementedError("Implement predict() method")
216''')
217
218    # Endpoint or pipeline based on template
219    if template in ("realtime", "both"):
220        (sdk_dir / "endpoint.py").write_text(f'''"""Endpoint definition for real-time serving."""
221
222from geronimo.serving import Endpoint
223
224# from .model import ProjectModel
225
226
227class PredictEndpoint(Endpoint):
228    """Prediction endpoint."""
229
230    # model_class = ProjectModel
231
232    def preprocess(self, request: dict):
233        """Preprocess incoming request."""
234        # df = pd.DataFrame([request["data"]])
235        # return self.model.features.transform(df)
236        raise NotImplementedError("Implement preprocess() method")
237
238    def postprocess(self, prediction):
239        """Postprocess model output."""
240        # return {{"score": float(prediction[0])}}
241        raise NotImplementedError("Implement postprocess() method")
242''')
243
244    if template in ("batch", "both"):
245        (sdk_dir / "pipeline.py").write_text(f'''"""Batch pipeline definition."""
246
247from geronimo.batch import BatchPipeline, Schedule
248
249# from .model import ProjectModel
250
251
252class ScoringPipeline(BatchPipeline):
253    """Batch scoring pipeline."""
254
255    # model_class = ProjectModel
256    schedule = Schedule.daily(hour=6)
257
258    def run(self):
259        """Main pipeline logic."""
260        # data = self.model.features.data_source.load()
261        # X = self.model.features.transform(data)
262        # predictions = self.model.predict(X)
263        # self.save_results(predictions)
264        raise NotImplementedError("Implement run() method")
265''')
266
267
268# ============================================================================
269# GENERATE Command Group
270# ============================================================================
271
272generate_app = typer.Typer(
273    name="generate",
274    help="Generate deployment artifacts (Terraform, Docker, pipelines).",
275    no_args_is_help=True,
276)
277app.add_typer(generate_app, name="generate")
278
279# Import and register keys CLI
280from geronimo.cli.keys_cmd import keys_app
281app.add_typer(keys_app, name="keys")
282
283# Import and register auth CLI
284from geronimo.cli.auth_cmd import auth_app
285app.add_typer(auth_app, name="auth")
286
287# Import and register config CLI
288from geronimo.cli.config_cmd import config_app
289app.add_typer(config_app, name="config")
290
291
292@generate_app.command("terraform")
293def generate_terraform(
294    config_path: str = typer.Option(
295        "geronimo.yaml",
296        "--config",
297        "-c",
298        help="Path to geronimo.yaml configuration file.",
299    ),
300    output_dir: str = typer.Option(
301        "infrastructure",
302        "--output",
303        "-o",
304        help="Output directory for Terraform files.",
305    ),
306) -> None:
307    """Generate Terraform infrastructure files.
308
309    Creates modular Terraform configuration for:
310    - ECR repository
311    - ECS Fargate task and service
312    - Application Load Balancer
313    - CloudWatch logging and monitoring
314    """
315    from geronimo.config.loader import load_config
316    from geronimo.generators.terraform import TerraformGenerator
317
318    console.print("\n[bold blue]Generating Terraform...[/bold blue]")
319
320    try:
321        config = load_config(config_path)
322        generator = TerraformGenerator(config=config, output_dir=output_dir)
323        files = generator.generate()
324
325        console.print(f"[green]✓ Generated {len(files)} Terraform files:[/green]")
326        for f in files:
327            console.print(f"  • {f}")
328
329    except FileNotFoundError:
330        console.print(
331            f"[bold red]Error:[/bold red] Config file not found: {config_path}"
332        )
333        raise typer.Exit(code=1)
334    except Exception as e:
335        console.print(f"[bold red]Error:[/bold red] {e}")
336        raise typer.Exit(code=1)
337
338
339@generate_app.command("dockerfile")
340def generate_dockerfile(
341    config_path: str = typer.Option(
342        "geronimo.yaml",
343        "--config",
344        "-c",
345        help="Path to geronimo.yaml configuration file.",
346    ),
347    output_path: str = typer.Option(
348        "Dockerfile",
349        "--output",
350        "-o",
351        help="Output path for Dockerfile.",
352    ),
353) -> None:
354    """Generate an optimized Dockerfile for ML serving.
355
356    Creates a multi-stage Dockerfile with:
357    - UV for fast dependency installation
358    - Non-root user for security
359    - Proper signal handling for graceful shutdown
360    """
361    from geronimo.config.loader import load_config
362    from geronimo.generators.docker import DockerGenerator
363
364    console.print("\n[bold blue]Generating Dockerfile...[/bold blue]")
365
366    try:
367        config = load_config(config_path)
368        generator = DockerGenerator(config=config, output_path=output_path)
369        generator.generate()
370
371        console.print(f"[green]✓ Generated Dockerfile:[/green] {output_path}")
372
373    except FileNotFoundError:
374        console.print(
375            f"[bold red]Error:[/bold red] Config file not found: {config_path}"
376        )
377        raise typer.Exit(code=1)
378    except Exception as e:
379        console.print(f"[bold red]Error:[/bold red] {e}")
380        raise typer.Exit(code=1)
381
382
383@generate_app.command("pipeline")
384def generate_pipeline(
385    config_path: str = typer.Option(
386        "geronimo.yaml",
387        "--config",
388        "-c",
389        help="Path to geronimo.yaml configuration file.",
390    ),
391    output_path: str = typer.Option(
392        "azure-pipelines.yaml",
393        "--output",
394        "-o",
395        help="Output path for pipeline file.",
396    ),
397) -> None:
398    """Generate CI/CD pipeline configuration.
399
400    Creates Azure DevOps pipeline YAML with:
401    - Build and test stage
402    - Security scanning
403    - Multi-environment deployments
404    - Approval gates
405    """
406    from geronimo.config.loader import load_config
407    from geronimo.generators.pipeline import PipelineGenerator
408
409    console.print("\n[bold blue]Generating pipeline...[/bold blue]")
410
411    try:
412        config = load_config(config_path)
413        generator = PipelineGenerator(config=config, output_path=output_path)
414        generator.generate()
415
416        console.print(f"[green]✓ Generated pipeline:[/green] {output_path}")
417
418    except FileNotFoundError:
419        console.print(
420            f"[bold red]Error:[/bold red] Config file not found: {config_path}"
421        )
422        raise typer.Exit(code=1)
423    except Exception as e:
424        console.print(f"[bold red]Error:[/bold red] {e}")
425        raise typer.Exit(code=1)
426
427
428@generate_app.command("all")
429def generate_all(
430    config_path: str = typer.Option(
431        "geronimo.yaml",
432        "--config",
433        "-c",
434        help="Path to geronimo.yaml configuration file.",
435    ),
436) -> None:
437    """Generate all deployment artifacts.
438
439    Generates Terraform, Dockerfile, and CI/CD pipeline in one command.
440    """
441    console.print("\n[bold blue]Generating all artifacts...[/bold blue]")
442
443    # Call individual generators
444    generate_terraform(config_path=config_path, output_dir="infrastructure")
445    generate_dockerfile(config_path=config_path, output_path="Dockerfile")
446    generate_pipeline(config_path=config_path, output_path="azure-pipelines.yaml")
447
448    console.print("\n[bold green]✓ All artifacts generated successfully![/bold green]")
449
450
451
452# ============================================================================
453# VALIDATE Command
454# ============================================================================
455
456
457@app.command()
458def validate(
459    config_path: str = typer.Option(
460        "geronimo.yaml",
461        "--config",
462        "-c",
463        help="Path to geronimo.yaml configuration file.",
464    ),
465) -> None:
466    """Validate project configuration against deployment rules.
467
468    Checks configuration for:
469    - Required fields
470    - Valid resource specifications
471    - Compliance with deployment policies
472    """
473    from geronimo.config.loader import load_config
474    from geronimo.validation.engine import ValidationEngine
475
476    console.print("\n[bold blue]Validating configuration...[/bold blue]")
477
478    try:
479        config = load_config(config_path)
480        engine = ValidationEngine()
481        results = engine.validate(config)
482
483        if results.is_valid:
484            console.print(
485                Panel.fit(
486                    "[bold green]✓ Configuration is valid![/bold green]\n"
487                    f"Checked {results.rules_checked} rules.",
488                    border_style="green",
489                )
490            )
491        else:
492            console.print("[bold red]✗ Validation failed:[/bold red]")
493            for error in results.errors:
494                console.print(f"  [red]•[/red] {error}")
495            raise typer.Exit(code=1)
496
497    except FileNotFoundError:
498        console.print(
499            f"[bold red]Error:[/bold red] Config file not found: {config_path}"
500        )
501        raise typer.Exit(code=1)
502    except Exception as e:
503        console.print(f"[bold red]Error:[/bold red] {e}")
504        raise typer.Exit(code=1)
505
506
507# NOTE: 'monitor' command removed - feature being redesigned
508
509
510
511# ============================================================================
512# DEPLOY Command Group
513# ============================================================================
514
515deploy_app = typer.Typer(
516    name="deploy",
517    help="Deploy infrastructure using Pulumi (requires: pip install geronimo[pulumi]).",
518    no_args_is_help=True,
519)
520app.add_typer(deploy_app, name="deploy")
521
522
523@deploy_app.command("up")
524def deploy_up(
525    project: str = typer.Option(
526        ...,
527        "--project",
528        "-p",
529        help="Project name.",
530    ),
531    target: str = typer.Option(
532        "aws",
533        "--target",
534        "-t",
535        help="Cloud target (aws, gcp, azure, cloud).",
536    ),
537    region: str = typer.Option(
538        "us-east-1",
539        "--region",
540        "-r",
541        help="Cloud region.",
542    ),
543    component: str = typer.Option(
544        None,
545        "--component",
546        "-c",
547        help="Specific component to deploy (artifacts, serving, batch).",
548    ),
549    stack: str = typer.Option(
550        "dev",
551        "--stack",
552        "-s",
553        help="Pulumi stack name.",
554    ),
555) -> None:
556    """Deploy infrastructure to the cloud.
557    
558    Requires Pulumi: pip install geronimo[pulumi]
559    
560    Examples:
561        geronimo deploy up --project iris --target aws
562        geronimo deploy up --project iris --component artifacts
563    """
564    from geronimo.deploy import DeploymentConfig, deploy
565    from geronimo.deploy.targets import PulumiNotInstalledError
566    
567    console.print(f"\n[bold blue]Deploying {project} to {target}...[/bold blue]")
568    
569    try:
570        config = DeploymentConfig(
571            project=project,
572            target=target,
573            region=region,
574            stack_name=stack,
575        )
576        
577        result = deploy(config, component=component)
578        
579        console.print(
580            Panel(
581                f"[green]✓ Deployment complete![/green]\n\n"
582                f"Stack: {stack}\n"
583                f"Outputs:\n" + "\n".join(f"  {k}: {v}" for k, v in result.get("outputs", {}).items()),
584                title="Deployment Success",
585                border_style="green",
586            )
587        )
588        
589    except PulumiNotInstalledError as e:
590        console.print(f"[bold yellow]Warning:[/bold yellow] {e}")
591        console.print("\nAlternatives:")
592        console.print("  1. Install Pulumi: [cyan]pip install geronimo[pulumi][/cyan]")
593        console.print("  2. Generate static IaC: [cyan]geronimo generate terraform[/cyan]")
594        raise typer.Exit(code=1)
595    except Exception as e:
596        console.print(f"[bold red]Error:[/bold red] {e}")
597        raise typer.Exit(code=1)
598
599
600@deploy_app.command("destroy")
601def deploy_destroy(
602    project: str = typer.Option(
603        ...,
604        "--project",
605        "-p",
606        help="Project name.",
607    ),
608    target: str = typer.Option(
609        "aws",
610        "--target",
611        "-t",
612        help="Cloud target.",
613    ),
614    stack: str = typer.Option(
615        "dev",
616        "--stack",
617        "-s",
618        help="Pulumi stack name.",
619    ),
620    force: bool = typer.Option(
621        False,
622        "--force",
623        "-f",
624        help="Skip confirmation prompt.",
625    ),
626) -> None:
627    """Destroy deployed infrastructure.
628    
629    Removes all resources created by 'deploy up'.
630    """
631    if not force:
632        confirm = typer.confirm(f"Destroy all resources for {project}/{stack}?")
633        if not confirm:
634            console.print("[yellow]Aborted.[/yellow]")
635            raise typer.Exit()
636    
637    console.print(f"\n[bold red]Destroying {project}/{stack}...[/bold red]")
638    
639    try:
640        from geronimo.deploy.config import DeploymentConfig
641        from geronimo.deploy.providers.aws import destroy_aws
642        
643        config = DeploymentConfig(
644            project=project,
645            target=target,
646            stack_name=stack,
647        )
648        
649        if target == "aws":
650            destroy_aws(config)
651        else:
652            console.print(f"[yellow]Destroy not implemented for {target}[/yellow]")
653            raise typer.Exit(code=1)
654        
655        console.print("[green]✓ Resources destroyed.[/green]")
656        
657    except Exception as e:
658        console.print(f"[bold red]Error:[/bold red] {e}")
659        raise typer.Exit(code=1)
660
661
662# NOTE: 'import' command removed - feature being redesigned
663
664
665# ============================================================================
666# DOCS Functions (for maintainer scripts, not exposed in CLI)
667# ============================================================================
668
669docs_app = typer.Typer(
670    name="docs",
671    help="Generate API documentation.",
672    no_args_is_help=True,
673)
674# NOTE: docs_app is intentionally NOT registered with app.add_typer()
675# End users don't need this command. Maintainers can run docs generation via:
676#   uv run python -m pdoc --output-directory docs/api geronimo
677# Or use the functions below directly in scripts.
678
679
680@docs_app.command("generate")
681def docs_generate(
682    output: str = typer.Option(
683        "docs/api",
684        "--output",
685        "-o",
686        help="Output directory for generated documentation.",
687    ),
688) -> None:
689    """Generate API documentation for the Geronimo library.
690
691    Uses pdoc to extract documentation from Python docstrings
692    and generate static HTML files.
693    """
694    import subprocess
695    import sys
696    from pathlib import Path
697
698    console.print("\n[bold blue]Generating API documentation...[/bold blue]")
699
700    # Find the project root (where pyproject.toml is)
701    current = Path(__file__).resolve()
702    project_root = current.parent.parent.parent.parent  # cli -> geronimo -> src -> root
703
704    # All public modules to document
705    modules = MODULES
706
707    output_dir = Path(output)
708    output_dir.mkdir(parents=True, exist_ok=True)
709
710    cmd = [
711        sys.executable,
712        "-m",
713        "pdoc",
714        "--output-directory",
715        str(output_dir),
716        *modules,
717    ]
718
719    # Set PYTHONPATH to include src
720    src_path = project_root / "src"
721    env = {**subprocess.os.environ, "PYTHONPATH": str(src_path)}
722
723    try:
724        result = subprocess.run(cmd, cwd=project_root, env=env, check=True)
725        console.print(
726            Panel.fit(
727                f"[bold green]✓ Documentation generated![/bold green]\n\n"
728                f"Output: [cyan]{output_dir.resolve()}[/cyan]\n"
729                f"Modules: {len(modules)}",
730                border_style="green",
731            )
732        )
733    except subprocess.CalledProcessError as e:
734        console.print(f"[bold red]Error:[/bold red] pdoc failed with exit code {e.returncode}")
735        console.print("[dim]Make sure pdoc is installed: pip install pdoc[/dim]")
736        raise typer.Exit(code=1)
737    except FileNotFoundError:
738        console.print("[bold red]Error:[/bold red] pdoc not found")
739        console.print("[dim]Install with: pip install pdoc[/dim]")
740        raise typer.Exit(code=1)
741
742
743@docs_app.command("serve")
744def docs_serve(
745    port: int = typer.Option(
746        8080,
747        "--port",
748        "-p",
749        help="Port to serve documentation on.",
750    ),
751) -> None:
752    """Start a live-reloading documentation server.
753
754    Opens a local development server that automatically
755    rebuilds documentation when source files change.
756    """
757    import subprocess
758    import sys
759    from pathlib import Path
760
761    console.print(f"\n[bold blue]Starting documentation server on port {port}...[/bold blue]")
762
763    # Find the project root
764    current = Path(__file__).resolve()
765    project_root = current.parent.parent.parent.parent
766
767    modules = MODULES
768
769    cmd = [
770        sys.executable,
771        "-m",
772        "pdoc",
773        "--host",
774        "localhost",
775        "--port",
776        str(port),
777        *modules,
778    ]
779
780    src_path = project_root / "src"
781    env = {**subprocess.os.environ, "PYTHONPATH": str(src_path)}
782
783    console.print(f"[dim]Open http://localhost:{port} in your browser[/dim]")
784    console.print("[dim]Press Ctrl+C to stop[/dim]\n")
785
786    try:
787        subprocess.run(cmd, cwd=project_root, env=env)
788    except KeyboardInterrupt:
789        console.print("\n[yellow]Server stopped.[/yellow]")
790
791
792if __name__ == "__main__":
793    app()
app = <typer.main.Typer object>
console = <console width=161 None>
def version_callback(value: bool) -> None:
27def version_callback(value: bool) -> None:
28    """Print version and exit."""
29    if value:
30        console.print(
31            Panel.fit(
32                f"[bold blue]Geronimo[/bold blue] v{__version__}\n"
33                "[dim]MLOps Deployment Platform[/dim]",
34                border_style="blue",
35            )
36        )
37        raise typer.Exit()

Print version and exit.

@app.callback()
def main(version: bool = <typer.models.OptionInfo object>) -> None:
40@app.callback()
41def main(
42    version: bool = typer.Option(
43        None,
44        "--version",
45        "-v",
46        callback=version_callback,
47        is_eager=True,
48        help="Show version and exit.",
49    ),
50) -> None:
51    """Geronimo - Automate ML model deployments to AWS.
52
53    Generate production-ready Terraform, Docker, and CI/CD pipelines
54    for your ML models with industry best practices built-in.
55    """
56    pass

Geronimo - Automate ML model deployments to AWS.

Generate production-ready Terraform, Docker, and CI/CD pipelines for your ML models with industry best practices built-in.

@app.command()
def init( name: str = <typer.models.OptionInfo object>, framework: str = <typer.models.OptionInfo object>, template: str = <typer.models.OptionInfo object>, output_dir: str = <typer.models.OptionInfo object>) -> None:
 64@app.command()
 65def init(
 66    name: str = typer.Option(
 67        None,
 68        "--name",
 69        "-n",
 70        prompt="Project name",
 71        help="Name of the ML project.",
 72    ),
 73    framework: str = typer.Option(
 74        "sklearn",
 75        "--framework",
 76        "-f",
 77        help="ML framework (sklearn, pytorch, tensorflow).",
 78    ),
 79    template: str = typer.Option(
 80        "realtime",
 81        "--template",
 82        "-t",
 83        help="Project type: 'realtime' (API endpoints), 'batch' (pipelines), or 'both'.",
 84    ),
 85    output_dir: str = typer.Option(
 86        ".",
 87        "--output",
 88        "-o",
 89        help="Output directory for the project.",
 90    ),
 91) -> None:
 92    """Initialize a new ML deployment project.
 93
 94    Scaffolds a complete ML project with Geronimo SDK:
 95
 96    Templates:
 97    - realtime: FastAPI endpoints with Endpoint class
 98    - batch: Metaflow pipelines with BatchPipeline class
 99    - both: Combined real-time and batch support
100    """
101    from geronimo.generators.project import ProjectGenerator
102
103    # Validate template
104    valid_templates = {"realtime", "batch", "both"}
105    if template not in valid_templates:
106        console.print(f"[bold red]Error:[/bold red] Invalid template '{template}'. Choose from: {valid_templates}")
107        raise typer.Exit(code=1)
108
109    console.print(f"\n[bold blue]Initializing project:[/bold blue] {name}")
110    console.print(f"  Template: [cyan]{template}[/cyan]")
111
112    generator = ProjectGenerator(
113        project_name=name,
114        framework=framework,
115        output_dir=output_dir,
116        template=template,
117    )
118
119    try:
120        generator.generate()
121
122        # SDK scaffolding is now handled by ProjectGenerator.generate()
123        # which creates sdk/endpoint.py, sdk/pipeline.py, app.py, flow.py, etc.
124
125        next_steps = [
126            f"cd {name}",
127            "uv sync",
128        ]
129        if template in ("realtime", "both"):
130            next_steps.append(f"uvicorn {name.replace('-', '_')}.app:app --reload  # Run API server")
131        if template in ("batch", "both"):
132            next_steps.append(f"python -m {name.replace('-', '_')}.flow run  # Run batch pipeline")
133
134        console.print(
135            Panel.fit(
136                f"[bold green]✓ Project '{name}' created successfully![/bold green]\n\n"
137                f"Template: [cyan]{template}[/cyan]\n\n"
138                f"Next steps:\n" + "\n".join(f"  {i+1}. {step}" for i, step in enumerate(next_steps)),
139                border_style="green",
140            )
141        )
142    except Exception as e:
143        console.print(f"[bold red]Error:[/bold red] {e}")
144        raise typer.Exit(code=1)

Initialize a new ML deployment project.

Scaffolds a complete ML project with Geronimo SDK:

Templates:

  • realtime: FastAPI endpoints with Endpoint class
  • batch: Metaflow pipelines with BatchPipeline class
  • both: Combined real-time and batch support
generate_app = <typer.main.Typer object>
@generate_app.command('terraform')
def generate_terraform( config_path: str = <typer.models.OptionInfo object>, output_dir: str = <typer.models.OptionInfo object>) -> None:
293@generate_app.command("terraform")
294def generate_terraform(
295    config_path: str = typer.Option(
296        "geronimo.yaml",
297        "--config",
298        "-c",
299        help="Path to geronimo.yaml configuration file.",
300    ),
301    output_dir: str = typer.Option(
302        "infrastructure",
303        "--output",
304        "-o",
305        help="Output directory for Terraform files.",
306    ),
307) -> None:
308    """Generate Terraform infrastructure files.
309
310    Creates modular Terraform configuration for:
311    - ECR repository
312    - ECS Fargate task and service
313    - Application Load Balancer
314    - CloudWatch logging and monitoring
315    """
316    from geronimo.config.loader import load_config
317    from geronimo.generators.terraform import TerraformGenerator
318
319    console.print("\n[bold blue]Generating Terraform...[/bold blue]")
320
321    try:
322        config = load_config(config_path)
323        generator = TerraformGenerator(config=config, output_dir=output_dir)
324        files = generator.generate()
325
326        console.print(f"[green]✓ Generated {len(files)} Terraform files:[/green]")
327        for f in files:
328            console.print(f"  • {f}")
329
330    except FileNotFoundError:
331        console.print(
332            f"[bold red]Error:[/bold red] Config file not found: {config_path}"
333        )
334        raise typer.Exit(code=1)
335    except Exception as e:
336        console.print(f"[bold red]Error:[/bold red] {e}")
337        raise typer.Exit(code=1)

Generate Terraform infrastructure files.

Creates modular Terraform configuration for:

  • ECR repository
  • ECS Fargate task and service
  • Application Load Balancer
  • CloudWatch logging and monitoring
@generate_app.command('dockerfile')
def generate_dockerfile( config_path: str = <typer.models.OptionInfo object>, output_path: str = <typer.models.OptionInfo object>) -> None:
340@generate_app.command("dockerfile")
341def generate_dockerfile(
342    config_path: str = typer.Option(
343        "geronimo.yaml",
344        "--config",
345        "-c",
346        help="Path to geronimo.yaml configuration file.",
347    ),
348    output_path: str = typer.Option(
349        "Dockerfile",
350        "--output",
351        "-o",
352        help="Output path for Dockerfile.",
353    ),
354) -> None:
355    """Generate an optimized Dockerfile for ML serving.
356
357    Creates a multi-stage Dockerfile with:
358    - UV for fast dependency installation
359    - Non-root user for security
360    - Proper signal handling for graceful shutdown
361    """
362    from geronimo.config.loader import load_config
363    from geronimo.generators.docker import DockerGenerator
364
365    console.print("\n[bold blue]Generating Dockerfile...[/bold blue]")
366
367    try:
368        config = load_config(config_path)
369        generator = DockerGenerator(config=config, output_path=output_path)
370        generator.generate()
371
372        console.print(f"[green]✓ Generated Dockerfile:[/green] {output_path}")
373
374    except FileNotFoundError:
375        console.print(
376            f"[bold red]Error:[/bold red] Config file not found: {config_path}"
377        )
378        raise typer.Exit(code=1)
379    except Exception as e:
380        console.print(f"[bold red]Error:[/bold red] {e}")
381        raise typer.Exit(code=1)

Generate an optimized Dockerfile for ML serving.

Creates a multi-stage Dockerfile with:

  • UV for fast dependency installation
  • Non-root user for security
  • Proper signal handling for graceful shutdown
@generate_app.command('pipeline')
def generate_pipeline( config_path: str = <typer.models.OptionInfo object>, output_path: str = <typer.models.OptionInfo object>) -> None:
384@generate_app.command("pipeline")
385def generate_pipeline(
386    config_path: str = typer.Option(
387        "geronimo.yaml",
388        "--config",
389        "-c",
390        help="Path to geronimo.yaml configuration file.",
391    ),
392    output_path: str = typer.Option(
393        "azure-pipelines.yaml",
394        "--output",
395        "-o",
396        help="Output path for pipeline file.",
397    ),
398) -> None:
399    """Generate CI/CD pipeline configuration.
400
401    Creates Azure DevOps pipeline YAML with:
402    - Build and test stage
403    - Security scanning
404    - Multi-environment deployments
405    - Approval gates
406    """
407    from geronimo.config.loader import load_config
408    from geronimo.generators.pipeline import PipelineGenerator
409
410    console.print("\n[bold blue]Generating pipeline...[/bold blue]")
411
412    try:
413        config = load_config(config_path)
414        generator = PipelineGenerator(config=config, output_path=output_path)
415        generator.generate()
416
417        console.print(f"[green]✓ Generated pipeline:[/green] {output_path}")
418
419    except FileNotFoundError:
420        console.print(
421            f"[bold red]Error:[/bold red] Config file not found: {config_path}"
422        )
423        raise typer.Exit(code=1)
424    except Exception as e:
425        console.print(f"[bold red]Error:[/bold red] {e}")
426        raise typer.Exit(code=1)

Generate CI/CD pipeline configuration.

Creates Azure DevOps pipeline YAML with:

  • Build and test stage
  • Security scanning
  • Multi-environment deployments
  • Approval gates
@generate_app.command('all')
def generate_all(config_path: str = <typer.models.OptionInfo object>) -> None:
429@generate_app.command("all")
430def generate_all(
431    config_path: str = typer.Option(
432        "geronimo.yaml",
433        "--config",
434        "-c",
435        help="Path to geronimo.yaml configuration file.",
436    ),
437) -> None:
438    """Generate all deployment artifacts.
439
440    Generates Terraform, Dockerfile, and CI/CD pipeline in one command.
441    """
442    console.print("\n[bold blue]Generating all artifacts...[/bold blue]")
443
444    # Call individual generators
445    generate_terraform(config_path=config_path, output_dir="infrastructure")
446    generate_dockerfile(config_path=config_path, output_path="Dockerfile")
447    generate_pipeline(config_path=config_path, output_path="azure-pipelines.yaml")
448
449    console.print("\n[bold green]✓ All artifacts generated successfully![/bold green]")

Generate all deployment artifacts.

Generates Terraform, Dockerfile, and CI/CD pipeline in one command.

@app.command()
def validate(config_path: str = <typer.models.OptionInfo object>) -> None:
458@app.command()
459def validate(
460    config_path: str = typer.Option(
461        "geronimo.yaml",
462        "--config",
463        "-c",
464        help="Path to geronimo.yaml configuration file.",
465    ),
466) -> None:
467    """Validate project configuration against deployment rules.
468
469    Checks configuration for:
470    - Required fields
471    - Valid resource specifications
472    - Compliance with deployment policies
473    """
474    from geronimo.config.loader import load_config
475    from geronimo.validation.engine import ValidationEngine
476
477    console.print("\n[bold blue]Validating configuration...[/bold blue]")
478
479    try:
480        config = load_config(config_path)
481        engine = ValidationEngine()
482        results = engine.validate(config)
483
484        if results.is_valid:
485            console.print(
486                Panel.fit(
487                    "[bold green]✓ Configuration is valid![/bold green]\n"
488                    f"Checked {results.rules_checked} rules.",
489                    border_style="green",
490                )
491            )
492        else:
493            console.print("[bold red]✗ Validation failed:[/bold red]")
494            for error in results.errors:
495                console.print(f"  [red]•[/red] {error}")
496            raise typer.Exit(code=1)
497
498    except FileNotFoundError:
499        console.print(
500            f"[bold red]Error:[/bold red] Config file not found: {config_path}"
501        )
502        raise typer.Exit(code=1)
503    except Exception as e:
504        console.print(f"[bold red]Error:[/bold red] {e}")
505        raise typer.Exit(code=1)

Validate project configuration against deployment rules.

Checks configuration for:

  • Required fields
  • Valid resource specifications
  • Compliance with deployment policies
deploy_app = <typer.main.Typer object>
@deploy_app.command('up')
def deploy_up( project: str = <typer.models.OptionInfo object>, target: str = <typer.models.OptionInfo object>, region: str = <typer.models.OptionInfo object>, component: str = <typer.models.OptionInfo object>, stack: str = <typer.models.OptionInfo object>) -> None:
524@deploy_app.command("up")
525def deploy_up(
526    project: str = typer.Option(
527        ...,
528        "--project",
529        "-p",
530        help="Project name.",
531    ),
532    target: str = typer.Option(
533        "aws",
534        "--target",
535        "-t",
536        help="Cloud target (aws, gcp, azure, cloud).",
537    ),
538    region: str = typer.Option(
539        "us-east-1",
540        "--region",
541        "-r",
542        help="Cloud region.",
543    ),
544    component: str = typer.Option(
545        None,
546        "--component",
547        "-c",
548        help="Specific component to deploy (artifacts, serving, batch).",
549    ),
550    stack: str = typer.Option(
551        "dev",
552        "--stack",
553        "-s",
554        help="Pulumi stack name.",
555    ),
556) -> None:
557    """Deploy infrastructure to the cloud.
558    
559    Requires Pulumi: pip install geronimo[pulumi]
560    
561    Examples:
562        geronimo deploy up --project iris --target aws
563        geronimo deploy up --project iris --component artifacts
564    """
565    from geronimo.deploy import DeploymentConfig, deploy
566    from geronimo.deploy.targets import PulumiNotInstalledError
567    
568    console.print(f"\n[bold blue]Deploying {project} to {target}...[/bold blue]")
569    
570    try:
571        config = DeploymentConfig(
572            project=project,
573            target=target,
574            region=region,
575            stack_name=stack,
576        )
577        
578        result = deploy(config, component=component)
579        
580        console.print(
581            Panel(
582                f"[green]✓ Deployment complete![/green]\n\n"
583                f"Stack: {stack}\n"
584                f"Outputs:\n" + "\n".join(f"  {k}: {v}" for k, v in result.get("outputs", {}).items()),
585                title="Deployment Success",
586                border_style="green",
587            )
588        )
589        
590    except PulumiNotInstalledError as e:
591        console.print(f"[bold yellow]Warning:[/bold yellow] {e}")
592        console.print("\nAlternatives:")
593        console.print("  1. Install Pulumi: [cyan]pip install geronimo[pulumi][/cyan]")
594        console.print("  2. Generate static IaC: [cyan]geronimo generate terraform[/cyan]")
595        raise typer.Exit(code=1)
596    except Exception as e:
597        console.print(f"[bold red]Error:[/bold red] {e}")
598        raise typer.Exit(code=1)

Deploy infrastructure to the cloud.

Requires Pulumi: pip install geronimo[pulumi]

Examples: geronimo deploy up --project iris --target aws geronimo deploy up --project iris --component artifacts

@deploy_app.command('destroy')
def deploy_destroy( project: str = <typer.models.OptionInfo object>, target: str = <typer.models.OptionInfo object>, stack: str = <typer.models.OptionInfo object>, force: bool = <typer.models.OptionInfo object>) -> None:
601@deploy_app.command("destroy")
602def deploy_destroy(
603    project: str = typer.Option(
604        ...,
605        "--project",
606        "-p",
607        help="Project name.",
608    ),
609    target: str = typer.Option(
610        "aws",
611        "--target",
612        "-t",
613        help="Cloud target.",
614    ),
615    stack: str = typer.Option(
616        "dev",
617        "--stack",
618        "-s",
619        help="Pulumi stack name.",
620    ),
621    force: bool = typer.Option(
622        False,
623        "--force",
624        "-f",
625        help="Skip confirmation prompt.",
626    ),
627) -> None:
628    """Destroy deployed infrastructure.
629    
630    Removes all resources created by 'deploy up'.
631    """
632    if not force:
633        confirm = typer.confirm(f"Destroy all resources for {project}/{stack}?")
634        if not confirm:
635            console.print("[yellow]Aborted.[/yellow]")
636            raise typer.Exit()
637    
638    console.print(f"\n[bold red]Destroying {project}/{stack}...[/bold red]")
639    
640    try:
641        from geronimo.deploy.config import DeploymentConfig
642        from geronimo.deploy.providers.aws import destroy_aws
643        
644        config = DeploymentConfig(
645            project=project,
646            target=target,
647            stack_name=stack,
648        )
649        
650        if target == "aws":
651            destroy_aws(config)
652        else:
653            console.print(f"[yellow]Destroy not implemented for {target}[/yellow]")
654            raise typer.Exit(code=1)
655        
656        console.print("[green]✓ Resources destroyed.[/green]")
657        
658    except Exception as e:
659        console.print(f"[bold red]Error:[/bold red] {e}")
660        raise typer.Exit(code=1)

Destroy deployed infrastructure.

Removes all resources created by 'deploy up'.

docs_app = <typer.main.Typer object>
@docs_app.command('generate')
def docs_generate(output: str = <typer.models.OptionInfo object>) -> None:
681@docs_app.command("generate")
682def docs_generate(
683    output: str = typer.Option(
684        "docs/api",
685        "--output",
686        "-o",
687        help="Output directory for generated documentation.",
688    ),
689) -> None:
690    """Generate API documentation for the Geronimo library.
691
692    Uses pdoc to extract documentation from Python docstrings
693    and generate static HTML files.
694    """
695    import subprocess
696    import sys
697    from pathlib import Path
698
699    console.print("\n[bold blue]Generating API documentation...[/bold blue]")
700
701    # Find the project root (where pyproject.toml is)
702    current = Path(__file__).resolve()
703    project_root = current.parent.parent.parent.parent  # cli -> geronimo -> src -> root
704
705    # All public modules to document
706    modules = MODULES
707
708    output_dir = Path(output)
709    output_dir.mkdir(parents=True, exist_ok=True)
710
711    cmd = [
712        sys.executable,
713        "-m",
714        "pdoc",
715        "--output-directory",
716        str(output_dir),
717        *modules,
718    ]
719
720    # Set PYTHONPATH to include src
721    src_path = project_root / "src"
722    env = {**subprocess.os.environ, "PYTHONPATH": str(src_path)}
723
724    try:
725        result = subprocess.run(cmd, cwd=project_root, env=env, check=True)
726        console.print(
727            Panel.fit(
728                f"[bold green]✓ Documentation generated![/bold green]\n\n"
729                f"Output: [cyan]{output_dir.resolve()}[/cyan]\n"
730                f"Modules: {len(modules)}",
731                border_style="green",
732            )
733        )
734    except subprocess.CalledProcessError as e:
735        console.print(f"[bold red]Error:[/bold red] pdoc failed with exit code {e.returncode}")
736        console.print("[dim]Make sure pdoc is installed: pip install pdoc[/dim]")
737        raise typer.Exit(code=1)
738    except FileNotFoundError:
739        console.print("[bold red]Error:[/bold red] pdoc not found")
740        console.print("[dim]Install with: pip install pdoc[/dim]")
741        raise typer.Exit(code=1)

Generate API documentation for the Geronimo library.

Uses pdoc to extract documentation from Python docstrings and generate static HTML files.

@docs_app.command('serve')
def docs_serve(port: int = <typer.models.OptionInfo object>) -> None:
744@docs_app.command("serve")
745def docs_serve(
746    port: int = typer.Option(
747        8080,
748        "--port",
749        "-p",
750        help="Port to serve documentation on.",
751    ),
752) -> None:
753    """Start a live-reloading documentation server.
754
755    Opens a local development server that automatically
756    rebuilds documentation when source files change.
757    """
758    import subprocess
759    import sys
760    from pathlib import Path
761
762    console.print(f"\n[bold blue]Starting documentation server on port {port}...[/bold blue]")
763
764    # Find the project root
765    current = Path(__file__).resolve()
766    project_root = current.parent.parent.parent.parent
767
768    modules = MODULES
769
770    cmd = [
771        sys.executable,
772        "-m",
773        "pdoc",
774        "--host",
775        "localhost",
776        "--port",
777        str(port),
778        *modules,
779    ]
780
781    src_path = project_root / "src"
782    env = {**subprocess.os.environ, "PYTHONPATH": str(src_path)}
783
784    console.print(f"[dim]Open http://localhost:{port} in your browser[/dim]")
785    console.print("[dim]Press Ctrl+C to stop[/dim]\n")
786
787    try:
788        subprocess.run(cmd, cwd=project_root, env=env)
789    except KeyboardInterrupt:
790        console.print("\n[yellow]Server stopped.[/yellow]")

Start a live-reloading documentation server.

Opens a local development server that automatically rebuilds documentation when source files change.