Skip to content

anypinn.cli.app

Typer CLI application for anypinn.

app = Typer(add_completion=False, context_settings={'help_option_names': ['-h', '--help']}) module-attribute

create(project_name: Annotated[str, Argument(help='Name for the new project directory')] = '.', template_str: Annotated[str | None, Option('--template', '-t', help='Project template. Run with --list-templates / -l to see all options.', show_default=False)] = None, data_source: Annotated[DataSource | None, Option('--data', '-d', help='Training data source')] = None, direction_str: Annotated[str | None, Option('--direction', help='Problem direction (forward or inverse). Only for PDE models.')] = None, lightning: Annotated[bool | None, Option('--lightning/--no-lightning', '-L/-NL', help='Include Lightning wrapper')] = None, run: Annotated[bool, Option('--run/--no-run', help='Install dependencies and run training after scaffolding')] = True, list_templates: Annotated[bool, Option('--list-templates', '-l', help='List all available templates and exit.', callback=_list_templates_callback, is_eager=True, expose_value=False)] = False) -> None

Create a new PINN project.

Source code in src/anypinn/cli/app.py
@app.command()
def create(
    project_name: Annotated[str, Argument(help="Name for the new project directory")] = ".",
    template_str: Annotated[
        str | None,
        Option(
            "--template",
            "-t",
            help="Project template. Run with --list-templates / -l to see all options.",
            show_default=False,
        ),
    ] = None,
    data_source: Annotated[
        DataSource | None, Option("--data", "-d", help="Training data source")
    ] = None,
    direction_str: Annotated[
        str | None,
        Option("--direction", help="Problem direction (forward or inverse). Only for PDE models."),
    ] = None,
    lightning: Annotated[
        bool | None,
        Option("--lightning/--no-lightning", "-L/-NL", help="Include Lightning wrapper"),
    ] = None,
    run: Annotated[
        bool,
        Option("--run/--no-run", help="Install dependencies and run training after scaffolding"),
    ] = True,
    list_templates: Annotated[
        bool,
        Option(
            "--list-templates",
            "-l",
            help="List all available templates and exit.",
            callback=_list_templates_callback,
            is_eager=True,
            expose_value=False,
        ),
    ] = False,
) -> None:
    """Create a new PINN project."""
    project_dir = Path(project_name).resolve()
    display_name = project_dir.name
    use_cwd = project_name == "."

    # Validate template early (non-interactive fast fail)
    template: Template | None = None
    if template_str is not None:
        try:
            template = Template(template_str)
        except ValueError:
            valid = ", ".join(f"'{t.value}'" for t in Template)
            _console.print()
            _console.print(
                f"[bold red]Error:[/] [bold]{template_str!r}[/] is not a valid template."
            )
            _console.print(f"[dim]Valid values:[/] {valid}")
            _print_templates()
            raise Exit(code=2) from None

    # Header
    _console.print()
    _console.print(f"[bold cyan]●[/]  anypinn v{anypinn.__version__}")
    _console.print("[dim]│[/]")

    # Handle existing directory
    if project_dir.exists():
        contents = list(project_dir.iterdir())
        if contents:
            if not _confirm(
                f"Directory '{display_name}' is not empty. Delete all contents?",
                default=False,
            ):
                _console.print()
                raise Exit(code=1)
            if not _confirm(
                "Are you sure? This cannot be undone",
                default=False,
            ):
                _console.print()
                raise Exit(code=1)
            for item in contents:
                if item.is_dir():
                    shutil.rmtree(item)
                else:
                    item.unlink()

    _console.print(f"[bold green]◇[/]  Generating project in '{display_name}'...")
    _console.print("[dim]│[/]")

    # Interactive prompts for missing options
    if template is None:
        template = prompt_template()
    else:
        _console.print("[bold green]◇[/]  Choose a starting point")
        _console.print(f"[dim]│[/]  {template.label}")
        _console.print("[dim]│[/]")

    # Direction prompt (only for PDE templates that support both)
    direction: Direction | None = None
    if template in TEMPLATES_WITH_DIRECTION:
        if direction_str is not None:
            try:
                direction = Direction(direction_str)
            except ValueError:
                valid = ", ".join(f"'{d.value}'" for d in Direction)
                _console.print(
                    f"[bold red]Error:[/] [bold]{direction_str!r}[/] is not a valid direction."
                )
                _console.print(f"[dim]Valid values:[/] {valid}")
                raise Exit(code=2) from None
            _console.print("[bold green]◇[/]  Problem direction")
            _console.print(f"[dim]│[/]  {direction.label}")
            _console.print("[dim]│[/]")
        else:
            direction = prompt_direction()

    if data_source is None:
        data_source = prompt_data_source()
    else:
        _console.print("[bold green]◇[/]  Select training data source")
        _console.print(f"[dim]│[/]  {data_source.label}")
        _console.print("[dim]│[/]")

    if lightning is None:
        lightning = prompt_lightning()
    else:
        display = "Yes" if lightning else "No"
        _console.print("[bold green]◇[/]  Include Lightning training wrapper?")
        _console.print(f"[dim]│[/]  {display}")
        _console.print("[dim]│[/]")

    # Render
    _console.print(f"[bold green]◇[/]  Creating '{display_name}/'")

    created = render_project(project_dir, template, data_source, lightning, direction)

    max_name = max(len(n) for n in created)
    for i, name in enumerate(created):
        desc = _FILE_DESCRIPTIONS.get(name, "")
        padded = name.ljust(max_name + 2)
        desc_str = f"{padded}[dim]{desc}[/]" if desc else name
        connector = "└" if i == len(created) - 1 else "├"
        _console.print(f"[dim]│[/]  {connector} {desc_str}")

    _console.print("[dim]│[/]")

    uv_path = shutil.which("uv") if run else None
    if uv_path is None:
        if use_cwd:
            _console.print("[bold cyan]●[/]  Done! uv sync && uv run train.py")
        else:
            _console.print(
                f"[bold cyan]●[/]  Done! cd {display_name} && uv sync && uv run train.py"
            )
        _console.print()
        return

    with _console.status(" Syncing dependencies...", spinner="dots", spinner_style="bold cyan"):
        proc = subprocess.run([uv_path, "sync"], capture_output=True, text=True, cwd=project_dir)

    if proc.returncode != 0:
        _console.print("[bold red]✗[/]  Failed to sync dependencies")
        if proc.stderr:
            _console.print(proc.stderr.strip())
        raise Exit(code=1)

    _console.print("[bold green]◇[/]  Dependencies synced")
    _console.print("[dim]│[/]")
    _console.print("[bold cyan]●[/]  Starting training with `uv run train.py`...")
    _console.print("   [dim]You can stop at any time and it will save the current checkpoint![/]")
    _console.print()

    os.chdir(project_dir)
    os.execvp(uv_path, [uv_path, "run", "train.py"])

main(version: Annotated[bool, Option('--version', '-v', help='Show version and exit.', callback=_version_callback, is_eager=True, expose_value=False)] = False) -> None

anypinn - scaffolding tool for Physics-Informed Neural Network projects.

Source code in src/anypinn/cli/app.py
@app.callback()
def main(
    version: Annotated[
        bool,
        Option(
            "--version",
            "-v",
            help="Show version and exit.",
            callback=_version_callback,
            is_eager=True,
            expose_value=False,
        ),
    ] = False,
) -> None:
    """anypinn - scaffolding tool for Physics-Informed Neural Network projects."""