diff --git a/mpcontribs-lux/README.md b/mpcontribs-lux/README.md index b64b47345..aae6070c1 100644 --- a/mpcontribs-lux/README.md +++ b/mpcontribs-lux/README.md @@ -42,3 +42,47 @@ print(pydantic_model.schema()) The test suite for MPContribs-lux will automatically test your `pydantic` models for arrow/parquet compatibility using the [arrowize](https://github.com/materialsproject/emmet/blob/74194bbf8c7b32ce15141bb1c9ee0527a0fc6c45/emmet-core/emmet/core/arrow.py#L40) utility from `emmet-core` (MP's production data model and data pipeline repository). Schemas submitted to MPContribs-lux do not necessarily need to be parquet/arrow compatlible, the `@arrow_incompatible` decorator from `emmet-core`'s [utils](https://github.com/materialsproject/emmet/blob/74194bbf8c7b32ce15141bb1c9ee0527a0fc6c45/emmet-core/emmet/core/utils.py#L104) module can be used to mark a `pydantic` model to be skipped during arrow compatibility testing. + +### Quickstart from the Command Line Interface (CLI) + +For this approach, `pip install -e 'mpcontribs-lux[cli]'` to first obtain a few extra dependencies needed to run the CLI. +Once done, run the CLI with `lux project scaffold` in the `MPContribs` directory. + +
+ +This will start a series of prompts asking how to structure your project: + + +```shell +/path/to/MPContribs > lux project scaffold +Name space for user project: JohnDoe +Structure of user project [directory, module]: module +Project names to scaffold in user project name space (space-separated list): project-1 project-2 project-3 +Include analysis module? [y/N]: y +Include pipeline module? [y/N]: n +Include project README.md? [y/N]: y +Include file for extra python requirements/libraries? [y/N]: y +The following project scaffold will be created in 'mpcontribs-lux.mpcontribs.projects': + JohnDoe +├── project-1 +│ ├── __init__.py +│ ├── analysis_1.py +│ ├── pip-extra-requirements.txt +│ ├── README.md +│ └── schema_1.py +├── project-2 +│ ├── __init__.py +│ ├── analysis_1.py +│ ├── pip-extra-requirements.txt +│ ├── README.md +│ └── schema_1.py +└── project-3 + ├── __init__.py + ├── analysis_1.py + ├── pip-extra-requirements.txt + ├── README.md + └── schema_1.py +Proceed? y/N [y/N]: +``` + +
diff --git a/mpcontribs-lux/mpcontribs/lux/cli/__init__.py b/mpcontribs-lux/mpcontribs/lux/cli/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mpcontribs-lux/mpcontribs/lux/cli/display/__init__.py b/mpcontribs-lux/mpcontribs/lux/cli/display/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mpcontribs-lux/mpcontribs/lux/cli/display/tree.py b/mpcontribs-lux/mpcontribs/lux/cli/display/tree.py new file mode 100644 index 000000000..da31a8b09 --- /dev/null +++ b/mpcontribs-lux/mpcontribs/lux/cli/display/tree.py @@ -0,0 +1,67 @@ +import pathlib +import tempfile + +from rich import print +from rich.markup import escape +from rich.text import Text +from rich.tree import Tree + +from mpcontribs.lux.cli.project.utils import build_scaffold + + +# github.com/textualize/rich/blob/master/examples/tree.py +def walk_directory(directory: pathlib.Path, tree: Tree) -> None: + """Recursively build a Tree with directory contents.""" + paths = sorted( + pathlib.Path(directory).iterdir(), + key=lambda path: (path.is_file(), path.name.lower()), + ) + for path in paths: + if path.name.startswith("."): + continue + if path.is_dir(): + style = "dim" if path.name.startswith("__") else "" + branch = tree.add( + f"[bold bright_blue] [link file://{path}]{escape(path.name)}", + style=style, + guide_style=style, + ) + walk_directory(path, branch) + else: + text_filename = Text(path.name, "white") + text_filename.stylize(f"link file://{path}") + tree.add(text_filename) + + +def visualize_scaffold( + user_space: str, + projects: set[str], + structure: str, + include_analysis: bool, + include_pipeline: bool, + include_readme: bool, + extra_reqs: bool, +): + tree = Tree( + f"[bold bright_blue] [link file://{user_space}]{user_space}", + guide_style="white", + ) + + user_space_root = pathlib.Path(__file__).parent.parent.parent.joinpath( + "projects", user_space + ) + + with tempfile.TemporaryDirectory(prefix=str(user_space_root)) as tmpdir: + build_scaffold( + pathlib.Path(tmpdir), + user_space, + projects, + structure, + include_analysis, + include_pipeline, + include_readme, + extra_reqs, + ) + + walk_directory(pathlib.Path(tmpdir), tree) + print(tree) diff --git a/mpcontribs-lux/mpcontribs/lux/cli/entry_point.py b/mpcontribs-lux/mpcontribs/lux/cli/entry_point.py new file mode 100644 index 000000000..004e1cfcf --- /dev/null +++ b/mpcontribs-lux/mpcontribs/lux/cli/entry_point.py @@ -0,0 +1,12 @@ +import click + +from mpcontribs.lux.cli.project import project +from mpcontribs.lux.cli.schema import schema + + +@click.group() +def lux(): ... + + +lux.add_command(project) +lux.add_command(schema) diff --git a/mpcontribs-lux/mpcontribs/lux/cli/project/__init__.py b/mpcontribs-lux/mpcontribs/lux/cli/project/__init__.py new file mode 100644 index 000000000..fd064fa28 --- /dev/null +++ b/mpcontribs-lux/mpcontribs/lux/cli/project/__init__.py @@ -0,0 +1,10 @@ +import click + +from mpcontribs.lux.cli.project.scaffold import scaffold + + +@click.group() +def project(): ... + + +project.add_command(scaffold) diff --git a/mpcontribs-lux/mpcontribs/lux/cli/project/scaffold.py b/mpcontribs-lux/mpcontribs/lux/cli/project/scaffold.py new file mode 100644 index 000000000..061a97e5f --- /dev/null +++ b/mpcontribs-lux/mpcontribs/lux/cli/project/scaffold.py @@ -0,0 +1,72 @@ +import pathlib + +import click + +from mpcontribs.lux.cli.display.tree import visualize_scaffold +from mpcontribs.lux.cli.project.utils import build_scaffold + + +@click.command() +@click.option("--user-space", default=None) +@click.option("--projects", multiple=True, default=None) +@click.option("--structure", type=click.Choice(["directory", "module"]), default=None) +@click.option("--include-analysis", is_flag=True, default=True) +@click.option("--include-pipeline", is_flag=True, default=True) +@click.option("--include-readme", is_flag=True, default=True) +@click.option("--extra-reqs", is_flag=True, default=True) +def scaffold( + user_space, + projects, + structure, + include_analysis, + include_pipeline, + include_readme, + extra_reqs, +): + if any([not x for x in (user_space, structure, projects)]): + user_space = click.prompt("Name space for user project") + structure = click.prompt("Structure of user project [directory, module]") + projects = click.prompt( + "Project names to scaffold in user project name space (space-separated list)" + ).split(" ") + include_analysis = click.confirm("Include analysis module?") + include_pipeline = click.confirm("Include pipeline module?") + include_readme = click.confirm("Include project README.md?") + extra_reqs = click.confirm( + "Include file for extra python requirements/libraries?" + ) + + projects = set(projects) + + click.echo( + "The following project scaffold will be created in 'mpcontribs-lux.mpcontribs.projects':" + ) + visualize_scaffold( + user_space, + projects, + structure, + include_analysis, + include_pipeline, + include_readme, + extra_reqs, + ) + if click.confirm("Proceed? y/N", abort=True): + user_space_root = pathlib.Path(__file__).parent.parent.parent.joinpath( + "projects", user_space + ) + + user_space_root.mkdir() + + build_scaffold( + user_space_root, + user_space, + projects, + structure, + include_analysis, + include_pipeline, + include_readme, + extra_reqs, + ) + click.echo( + f"Project scaffold created at 'mpcontribs-lux.mpcontribs.projects.{user_space}'!" + ) diff --git a/mpcontribs-lux/mpcontribs/lux/cli/project/utils.py b/mpcontribs-lux/mpcontribs/lux/cli/project/utils.py new file mode 100644 index 000000000..48c52335d --- /dev/null +++ b/mpcontribs-lux/mpcontribs/lux/cli/project/utils.py @@ -0,0 +1,50 @@ +import pathlib + + +def build_scaffold( + root, + user_space, + projects, + structure, + include_analysis, + include_pipeline, + include_readme, + extra_reqs, +): + for proj in projects: + proj_dir = root / proj + proj_dir.mkdir() + pathlib.Path(proj_dir, "__init__.py").touch() + + if include_readme: + pathlib.Path(proj_dir, "README.md").touch() + if extra_reqs: + pathlib.Path(proj_dir, "pip-extra-requirements.txt").touch() + + match structure: + case "directory": + schema_dir = pathlib.Path(proj_dir) / "schemas" + schema_dir.mkdir() + pathlib.Path(schema_dir, "__init__.py").touch() + pathlib.Path(schema_dir, "schema_1.py").touch() + + if include_analysis: + analysis_dir = pathlib.Path(proj_dir) / "analysis" + analysis_dir.mkdir() + pathlib.Path(analysis_dir, "__init__.py").touch() + pathlib.Path(analysis_dir, "analysis_1.py").touch() + + if include_pipeline: + pipeline_dir = pathlib.Path(proj_dir) / "pipelines" + pipeline_dir.mkdir() + pathlib.Path(pipeline_dir, "__init__.py").touch() + pathlib.Path(pipeline_dir, "pipeline_1.py").touch() + + case "module": + pathlib.Path(proj_dir, "schema_1.py").touch() + + if include_analysis: + pathlib.Path(proj_dir, "analysis_1.py").touch() + + if include_pipeline: + pathlib.Path(proj_dir, "pipeline_1.py").touch() diff --git a/mpcontribs-lux/mpcontribs/lux/cli/schema/__init__.py b/mpcontribs-lux/mpcontribs/lux/cli/schema/__init__.py new file mode 100644 index 000000000..4182b82b7 --- /dev/null +++ b/mpcontribs-lux/mpcontribs/lux/cli/schema/__init__.py @@ -0,0 +1,11 @@ +import click + +from mpcontribs.lux.cli.schema.autogen import autogen + + +@click.group() +def schema(): + pass + + +schema.add_command(autogen) diff --git a/mpcontribs-lux/mpcontribs/lux/cli/schema/autogen.py b/mpcontribs-lux/mpcontribs/lux/cli/schema/autogen.py new file mode 100644 index 000000000..2c5b83721 --- /dev/null +++ b/mpcontribs-lux/mpcontribs/lux/cli/schema/autogen.py @@ -0,0 +1,7 @@ +import click + + +@click.command() +def autogen(): + # TODO: SchemaGenerator(file).pydantic_schema from --file, write output to --output-file (?) + ... diff --git a/mpcontribs-lux/pyproject.toml b/mpcontribs-lux/pyproject.toml index c259b97ee..63d9ea08e 100644 --- a/mpcontribs-lux/pyproject.toml +++ b/mpcontribs-lux/pyproject.toml @@ -35,7 +35,14 @@ authors = [ Homepage = "https://github.com/materialsproject/MPContribs" Documentation = "https://docs.materialsproject.org/services/mpcontribs" +[project.scripts] +lux = "mpcontribs.lux.cli.entry_point:lux" + [project.optional-dependencies] +cli = [ + "click", + "rich", +] test = [ "pre-commit", "pytest",