Skip to content
Merged

Lux cli #1977

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions mpcontribs-lux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details>
<summary>
This will start a series of prompts asking how to structure your project:
</summary>

```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]:
```

</details>
Empty file.
Empty file.
67 changes: 67 additions & 0 deletions mpcontribs-lux/mpcontribs/lux/cli/display/tree.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions mpcontribs-lux/mpcontribs/lux/cli/entry_point.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 10 additions & 0 deletions mpcontribs-lux/mpcontribs/lux/cli/project/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import click

from mpcontribs.lux.cli.project.scaffold import scaffold


@click.group()
def project(): ...


project.add_command(scaffold)
72 changes: 72 additions & 0 deletions mpcontribs-lux/mpcontribs/lux/cli/project/scaffold.py
Original file line number Diff line number Diff line change
@@ -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}'!"
)
50 changes: 50 additions & 0 deletions mpcontribs-lux/mpcontribs/lux/cli/project/utils.py
Original file line number Diff line number Diff line change
@@ -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()
11 changes: 11 additions & 0 deletions mpcontribs-lux/mpcontribs/lux/cli/schema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import click

from mpcontribs.lux.cli.schema.autogen import autogen


@click.group()
def schema():
pass


schema.add_command(autogen)
7 changes: 7 additions & 0 deletions mpcontribs-lux/mpcontribs/lux/cli/schema/autogen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import click


@click.command()
def autogen():
# TODO: SchemaGenerator(file).pydantic_schema from --file, write output to --output-file (?)
...
7 changes: 7 additions & 0 deletions mpcontribs-lux/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down