diff --git a/foamlib/__main__.py b/foamlib/__main__.py new file mode 100644 index 00000000..40a94e54 --- /dev/null +++ b/foamlib/__main__.py @@ -0,0 +1,93 @@ +"""A command-line interface for the 'foamlib' package.""" + +from __future__ import annotations + +import asyncio +import sys +from pathlib import Path + +import typer + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + +from . import AsyncFoamCase, AsyncSlurmFoamCase, __version__ +from ._util import async_to_sync + +app = typer.Typer(help=__doc__) + + +@app.command() +@async_to_sync +async def run( + cases: Annotated[ + list[Path] | None, + typer.Argument(help="Case directories", show_default="."), + ] = None, + slurm: Annotated[ + bool | None, + typer.Option( + help="Use Slurm for running cases.", show_default="use Slurm if available" + ), + ] = None, + max_cpus: Annotated[ + int, + typer.Option( + help="Maximum number of concurrent processes (for non-Slurm runs).", + ), + ] = AsyncFoamCase.max_cpus, +) -> None: + """Run one or more OpenFOAM cases.""" + if cases is None: + cases = [Path.cwd()] + + AsyncFoamCase.max_cpus = max_cpus + + if slurm is None: + await asyncio.gather( + *(AsyncSlurmFoamCase(case).run(fallback=True) for case in cases) + ) + elif slurm: + await asyncio.gather(*(AsyncSlurmFoamCase(case).run() for case in cases)) + else: + await asyncio.gather(*(AsyncFoamCase(case).run() for case in cases)) + + +@app.command() +@async_to_sync +async def clean( + cases: Annotated[ + list[Path] | None, + typer.Argument(help="Case directories", show_default="."), + ], +) -> None: + """Clean one or more OpenFOAM cases.""" + if cases is None: + cases = [Path.cwd()] + + await asyncio.gather(*(AsyncFoamCase(case).clean() for case in cases)) + + +def _version_callback(*, show: bool) -> None: + if show: + typer.echo(f"foamlib {__version__}") + raise typer.Exit + + +@app.callback() +def common( # noqa: D103 + *, + version: Annotated[ + bool, + typer.Option( + "--version", help="Show version and exit.", callback=_version_callback + ), + ] = False, +) -> None: + pass + + +if __name__ == "__main__": + app() diff --git a/foamlib/_util.py b/foamlib/_util.py new file mode 100644 index 00000000..d4ef4e04 --- /dev/null +++ b/foamlib/_util.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import asyncio +import sys +from functools import wraps +from typing import Any, TypeVar + +if sys.version_info >= (3, 9): + from collections.abc import Callable, Coroutine +else: + from typing import Callable, Coroutine + +R = TypeVar("R") + + +def async_to_sync(coro: Callable[..., Coroutine[Any, Any, R]]) -> Callable[..., R]: + @wraps(coro) + def wrapper(*args: Any, **kwargs: Any) -> R: + return asyncio.run(coro(*args, **kwargs)) + + return wrapper diff --git a/pyproject.toml b/pyproject.toml index c76103d4..7030aeb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "numpy>=1,<3", "pyparsing>=3.1.2,<4", "rich>=13,<15", + "typer-slim>=0.13,<0.16", "typing-extensions>=4,<5; python_version<'3.11'", ] @@ -76,6 +77,9 @@ Homepage = "https://github.com/gerlero/foamlib" Repository = "https://github.com/gerlero/foamlib" Documentation = "https://foamlib.readthedocs.io" +[project.scripts] +foamlib = "foamlib.__main__:app" + [tool.hatch.version] path = "foamlib/__init__.py"