Skip to content

Commit

Permalink
feat: add init command to configure deployer (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
julesbertrand committed Jan 4, 2024
1 parent ea3a67d commit 5ac7022
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 19 deletions.
37 changes: 29 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@
<li><a href="#-cli-checking-pipelines-are-valid-with-check">CLI: Checking Pipelines are valid with `check`</a></li>
<li><a href="#-cli-other-commands">CLI: Other commands</a></li>
<ul>
<li><a href="#config">`config`</a></li>
<li><a href="#create">`create`</a></li>
<li><a href="#init">`init`</a></li>
<li><a href="#list">`list`</a></li>
<li><a href="#config">`config`</a></li>
</ul>
</ul>
<li><a href="#cli-options">CLI: Options</a></li>
Expand All @@ -71,8 +72,11 @@ Four commands:

- `check`: check your pipelines (imports, compile, check configs validity against pipeline definition).
- `deploy`: compile, upload to Artifact Registry, run and schedule your pipelines.
- `config`: display the configuration from `pyproject.toml`.
- `create`: create a new pipeline and config files.
- `init`: initialize the project with necessary configuration files and directory structure.
- `list`: list all pipelines in the `vertex/pipelines` folder.

<!-- --8<-- [end:why] -->

## 📋 Prerequisites
Expand Down Expand Up @@ -363,6 +367,14 @@ vertex-deployer check --all

### 🛠️ CLI: Other commands

#### `config`

You can check your `vertex-deployer` configuration options using the `config` command.
Fields set in `pyproject.toml` will overwrite default values and will be displayed differently:
```bash
vertex-deployer config --all
```

#### `create`

You can create all files needed for a pipeline using the `create` command:
Expand All @@ -372,19 +384,28 @@ vertex-deployer create my_new_pipeline --config-type py

This will create a `my_new_pipeline.py` file in the `vertex/pipelines` folder and a `vertex/config/my_new_pipeline/` folder with multiple config files in it.

#### `list`
#### `init`

You can list all pipelines in the `vertex/pipelines` folder using the `list` command:
To initialize the deployer with default settings and folder structure, use the `init` command:
```bash
vertex-deployer list --with-configs
vertex-deployer init
```

#### `config`
```bash
$ vertex-deployer init
Welcome to Vertex Deployer!
This command will help you getting fired up.
Do you want to configure the deployer? [y/n]: n
Do you want to build default folder structure [y/n]: n
Do you want to create a pipeline? [y/n]: n
All done
```

You can check your `vertex-deployer` configuration options using the `config` command.
Fields set in `pyproject.toml` will overwrite default values and will be displayed differently:
#### `list`

You can list all pipelines in the `vertex/pipelines` folder using the `list` command:
```bash
vertex-deployer config --all
vertex-deployer list --with-configs
```

### 🍭 CLI: Options
Expand Down
92 changes: 88 additions & 4 deletions deployer/cli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import sys
from pathlib import Path
from typing import List, Optional
Expand All @@ -6,6 +7,7 @@
import typer
from loguru import logger
from pydantic import ValidationError
from rich.prompt import Prompt
from typing_extensions import Annotated

from deployer.constants import (
Expand All @@ -15,13 +17,20 @@
PIPELINE_MINIMAL_TEMPLATE,
PYTHON_CONFIG_TEMPLATE,
)
from deployer.settings import load_deployer_settings
from deployer.settings import (
DeployerSettings,
find_pyproject_toml,
load_deployer_settings,
update_pyproject_toml,
)
from deployer.utils.config import (
ConfigType,
VertexPipelinesSettings,
list_config_filepaths,
load_config,
load_vertex_settings,
)
from deployer.utils.console import ask_user_for_model_fields
from deployer.utils.logging import LoguruLevel, console
from deployer.utils.utils import (
dict_to_repr,
Expand Down Expand Up @@ -280,7 +289,7 @@ def deploy( # noqa: C901
)


@app.command(no_args_is_help=True)
@app.command()
def check(
pipeline_name: Annotated[
PipelineName,
Expand Down Expand Up @@ -403,8 +412,8 @@ def list(
print_pipelines_list(pipelines_dict, with_configs)


@app.command(no_args_is_help=True)
def create(
@app.command(name="create")
def create_pipeline(
pipeline_name: Annotated[
str,
typer.Argument(..., help="The name of the pipeline to create."),
Expand All @@ -415,6 +424,12 @@ def create(
] = ConfigType.json,
):
"""Create files structure for a new pipeline."""
if not re.match(r"^[a-zA-Z0-9_]+$", pipeline_name):
raise typer.BadParameter(
f"Invalid Pipeline name: '{pipeline_name}'\n"
"Pipeline name must only contain alphanumeric characters and underscores"
)

logger.info(f"Creating pipeline {pipeline_name}")

for path in [deployer_settings.pipelines_root_path, deployer_settings.config_root_path]:
Expand Down Expand Up @@ -442,3 +457,72 @@ def create(
raise e

logger.success(f"Pipeline {pipeline_name} created with configs in {config_dirpath}")


@app.command(name="init")
def init_deployer(): # noqa: C901
console.print("Welcome to Vertex Deployer!", style="blue")
console.print("This command will help you getting fired up.", style="blue")

if Prompt.ask("Do you want to configure the deployer?", choices=["y", "n"]) == "y":
pyproject_toml_filepath = find_pyproject_toml(Path.cwd().resolve())

if pyproject_toml_filepath is None:
console.print(
"No pyproject.toml file found. Creating one in current directory.",
style="yellow",
)
pyproject_toml_filepath = Path("./pyproject.toml")
pyproject_toml_filepath.touch()

set_fields = ask_user_for_model_fields(DeployerSettings)

new_deployer_settings = DeployerSettings(**set_fields)

update_pyproject_toml(pyproject_toml_filepath, new_deployer_settings)
console.print("Configuration saved in pyproject.toml :sparkles:", style="blue")

if Prompt.ask("Do you want to build default folder structure", choices=["y", "n"]) == "y":

def create_file_or_dir(path: Path, text: str = ""):
"""Create a file (if text is provided) or a directory at path. Warn if path exists."""
if path.exists():
console.print(
f"Path '{path}' already exists. Skipping creation of path.", style="yellow"
)
else:
if text:
path.touch()
path.write_text(text)
else:
path.mkdir(parents=True)

create_file_or_dir(deployer_settings.pipelines_root_path)
create_file_or_dir(deployer_settings.config_root_path)
create_file_or_dir(
Path("./.env"), "=\n".join(VertexPipelinesSettings.model_json_schema()["required"])
)

if Prompt.ask("Do you want to create a pipeline?", choices=["y", "n"]) == "y":
wrong_name = True
while wrong_name:
pipeline_name = Prompt.ask("What is the name of the pipeline?")
pipeline_path = Path(deployer_settings.pipelines_root_path) / f"{pipeline_name}.py"

try:
create_pipeline(pipeline_name=pipeline_name)
except typer.BadParameter as e:
console.print(e, style="red")
except FileExistsError:
console.print(
f"Pipeline '{pipeline_name}' already exists. Skipping creation.",
style="yellow",
)
else:
wrong_name = False
console.print(
f"Pipeline '{pipeline_name}' created at '{pipeline_path}'. :sparkles:",
style="blue",
)

console.print("All done :sparkles:", style="blue")
8 changes: 5 additions & 3 deletions deployer/constants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
PIPELINE_ROOT_PATH = "vertex/pipelines"
CONFIG_ROOT_PATH = "vertex/configs"
from pathlib import Path

PIPELINE_ROOT_PATH = Path("vertex/pipelines")
CONFIG_ROOT_PATH = Path("vertex/configs")

DEFAULT_SCHEDULER_TIMEZONE = "Europe/Paris"
DEFAULT_LOCAL_PACKAGE_PATH = "vertex/pipelines/compiled_pipelines"
DEFAULT_LOCAL_PACKAGE_PATH = Path("vertex/pipelines/compiled_pipelines")
DEFAULT_TAGS = None

TEMP_LOCAL_PACKAGE_PATH = ".vertex-deployer-temp"
Expand Down
31 changes: 31 additions & 0 deletions deployer/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
from typing import Any, Dict, List, Optional

import toml
import tomlkit
from loguru import logger
from pydantic import ValidationError
from tomlkit.toml_file import TOMLFile

from deployer import constants
from deployer.utils.config import ConfigType
Expand Down Expand Up @@ -87,6 +89,35 @@ def parse_pyproject_toml(path_pyproject_toml: str) -> Dict[str, Any]:
return settings


def update_pyproject_toml(path_pyproject_toml: str, deployer_settings: DeployerSettings) -> None:
"""Update the pyproject.toml file with the non-default fields from the deployer configuration.
Args:
path_pyproject_toml (str): The file path to the pyproject.toml file.
deployer_settings (DeployerSettings): The deployer configuration instance with potential
updates.
"""
toml_file = TOMLFile(path_pyproject_toml)
toml_document = toml_file.read()

non_default_fields = deployer_settings.model_dump(mode="json", exclude_unset=True)

root_keys = ["tool", "vertex_deployer"]

section = toml_document
for key in root_keys:
if key not in section:
# if section is OutOfOrderTableProxy, no append method is available
# Then it can mess up the order of the keys
# But no other solution found
section[key] = tomlkit.table(is_super_table=True)
section = section[key]

section.update(non_default_fields)

toml_file.write(toml_document)


@lru_cache()
def load_deployer_settings() -> DeployerSettings:
"""Load the settings for Vertex Deployer."""
Expand Down
47 changes: 47 additions & 0 deletions deployer/utils/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from enum import Enum
from inspect import isclass
from typing import Type

from pydantic import BaseModel
from rich.prompt import Prompt


def ask_user_for_model_fields(model: Type[BaseModel]) -> dict:
"""Ask user for model fields and return a dictionary with the results.
Args:
model (Type[BaseModel]): The pydantic model to configure.
Returns:
dict: A dictionary of the set fields.
"""
set_fields = {}
for field_name, field_info in model.model_fields.items():
if isclass(field_info.annotation) and issubclass(field_info.annotation, BaseModel):
answer = Prompt.ask(
f"Do you want to configure command {field_name}?", choices=["y", "n"], default="n"
)
if answer == "y":
set_fields[field_name] = ask_user_for_model_fields(field_info.annotation)

else:
annotation = field_info.annotation
default = field_info.default
choices = None

if isclass(annotation) and issubclass(annotation, Enum):
choices = list(annotation.__members__)

if isclass(annotation) and annotation == bool:
choices = ["y", "n"]
default = "y" if field_info.default else "n"

answer = Prompt.ask(field_name, default=default, choices=choices)

if isclass(annotation) and annotation == bool:
answer = answer == "y"

if answer != field_info.default:
set_fields[field_name] = answer

return set_fields
5 changes: 2 additions & 3 deletions deployer/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ def __call__( # noqa: D102

def import_pipeline_from_dir(dirpath: Path, pipeline_name: str) -> GraphComponentType:
"""Import a pipeline from a directory."""
if dirpath.startswith("."):
dirpath = dirpath[1:]
parent_module = ".".join(Path(dirpath).parts)
dirpath_ = Path(dirpath).resolve().relative_to(Path.cwd())
parent_module = ".".join(dirpath_.parts)
module_path = f"{parent_module}.{pipeline_name}"

try:
Expand Down
15 changes: 15 additions & 0 deletions docs/CLI_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ $ vertex-deployer [OPTIONS] COMMAND [ARGS]...
* `config`: Display the configuration from pyproject.toml.
* `create`: Create files structure for a new pipeline.
* `deploy`: Compile, upload, run and schedule pipelines.
* `init`: Initialize the deployer with default settings and folder structure.
* `list`: List all pipelines.

## `vertex-deployer check`
Expand Down Expand Up @@ -119,6 +120,20 @@ $ vertex-deployer deploy [OPTIONS] PIPELINE_NAME
* `--local-package-path, -lpp PATH`: Local dir path where pipelines will be compiled. [default: vertex/pipelines/compiled_pipelines]
* `--help`: Show this message and exit.

## `vertex-deployer init`

Initialize the deployer with default settings and folder structure.

**Usage**:

```console
$ vertex-deployer init
```

**Options**:

* `--help`: Show this message and exit.

## `vertex-deployer list`

List all pipelines.
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def get_typer_app_signature(app: typer.Typer):
for cmd in registered_commands:
cmd_func = cmd.callback
cmd_parameters = signature(cmd_func).parameters
cmd_name = cmd_func.__name__
cmd_name = cmd.name or cmd_func.__name__

for param in cmd_parameters.values():
annotation = param.annotation
Expand Down
Loading

0 comments on commit 5ac7022

Please sign in to comment.