Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create init command #394

Merged
merged 42 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c0810f0
feat: kpops init creates empty files
sujuka99 Oct 24, 2023
2c50e27
feat: collect config fields WIP
sujuka99 Oct 24, 2023
96cfb82
Merge remote-tracking branch 'origin/main' into feat/kpops-init
sujuka99 Dec 20, 2023
efb6032
feat: utils for creating project files WIP
sujuka99 Dec 21, 2023
8c0363e
Merge remote-tracking branch 'origin/v3' into feat/kpops-init
sujuka99 Dec 21, 2023
782481f
fix: errors
sujuka99 Dec 21, 2023
886485b
feat: generate config.yaml WIP
sujuka99 Dec 22, 2023
f3b5e17
refactor: config.yaml generation WIP
sujuka99 Jan 8, 2024
2b51ed8
Merge remote-tracking branch 'origin/main' into feat/kpops-init
sujuka99 Jan 26, 2024
22ca728
feat: kpops-init creates a config.yaml
sujuka99 Jan 26, 2024
d35a4aa
generate defaults WIP
sujuka99 Feb 5, 2024
344e093
Merge remote-tracking branch 'origin/main' into feat/kpops-init
sujuka99 Feb 12, 2024
267a0b4
Create defaults and pipeline yaml
sujuka99 Feb 13, 2024
6502f6f
Interactive init
sujuka99 Feb 14, 2024
27c4617
Get rid of temp tests
sujuka99 Feb 14, 2024
5f82d86
Narow down the allowed types WIP
sujuka99 Feb 14, 2024
acd7a29
Get rid of unneeded functionality
sujuka99 Feb 14, 2024
ffa50c8
simplify WIP
sujuka99 Feb 15, 2024
8e44c7a
Lint
sujuka99 Feb 23, 2024
59a762a
Clean up
sujuka99 Feb 23, 2024
badc072
Add flag to include non-required fields in generated config.yaml
sujuka99 Feb 23, 2024
72cd132
Add docstrings to new code
sujuka99 Feb 26, 2024
e7b4517
Cosmetic
sujuka99 Feb 26, 2024
b58757c
Test new features
sujuka99 Feb 28, 2024
5369aac
Merge remote-tracking branch 'origin/main' into feat/kpops-init
sujuka99 Feb 28, 2024
b6296a5
Cosmetic
sujuka99 Feb 28, 2024
3bc684b
Clean up
sujuka99 Feb 28, 2024
b4b7401
Remove `--name` flag from `kpops init`
sujuka99 Feb 29, 2024
61c9903
Revert ignore comments changes
sujuka99 Feb 29, 2024
1716786
Merge remote-tracking branch 'origin/main' into feat/kpops-init
sujuka99 Feb 29, 2024
4fbb674
Remove `touch_yaml` function
sujuka99 Feb 29, 2024
5a57da4
Add case for default value being a BaseModel instance
sujuka99 Mar 1, 2024
b7b18e9
Fix return type and docstring
sujuka99 Mar 1, 2024
f4d2420
Add `kpops init` to the public API
sujuka99 Mar 1, 2024
8fc749a
Add tests
sujuka99 Mar 1, 2024
a61fbaf
Delete unused function
sujuka99 Mar 1, 2024
f38509d
Merge remote-tracking branch 'origin/main' into feat/kpops-init
sujuka99 Mar 6, 2024
5d1f830
Remove parentheses
sujuka99 Mar 6, 2024
83e9db2
Move kpops init tests
sujuka99 Mar 6, 2024
6cd9731
Merge branch 'feat/kpops-init' of github.com:bakdata/kpops into feat/…
sujuka99 Mar 6, 2024
0f5d292
Merge remote-tracking branch 'origin/main' into feat/kpops-init
sujuka99 Mar 7, 2024
3412821
Test `kpops-init` using snapshots
sujuka99 Mar 7, 2024
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
20 changes: 20 additions & 0 deletions docs/docs/user/references/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ $ kpops [OPTIONS] COMMAND [ARGS]...
* `deploy`: Deploy pipeline steps
* `destroy`: Destroy pipeline steps
* `generate`: Generate enriched pipeline representation
* `init`: Initialize a new KPOps project.
* `manifest`: Render final resource representation
* `reset`: Reset pipeline steps
* `schema`: Generate JSON schema.
Expand Down Expand Up @@ -126,6 +127,25 @@ $ kpops generate [OPTIONS] PIPELINE_PATH
* `--verbose / --no-verbose`: Enable verbose printing [default: no-verbose]
* `--help`: Show this message and exit.

## `kpops init`

Initialize a new KPOps project.

**Usage**:

```console
$ kpops init [OPTIONS] PATH
```

**Arguments**:

* `PATH`: Path for a new KPOps project. It should lead to an empty (or non-existent) directory. The part of the path that doesn't exist will be created. [required]

**Options**:

* `--config-include-opt / --no-config-include-opt`: Whether to include non-required settings in the generated 'config.yaml' [default: no-config-include-opt]
* `--help`: Show this message and exit.

## `kpops manifest`

In addition to generate, render final resource representation for each pipeline step, e.g. Kubernetes manifests.
Expand Down
1 change: 0 additions & 1 deletion hooks/gen_docs/gen_docs_env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ def write_csv_to_md_file(
:param source: path to csv file to read from
:param target: path to md file to overwrite or create
:param title: Title for the table, optional

"""
if heading:
heading += " "
Expand Down
3 changes: 2 additions & 1 deletion kpops/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
__version__ = "4.0.1"

# export public API functions
from kpops.cli.main import clean, deploy, destroy, generate, manifest, reset
from kpops.cli.main import clean, deploy, destroy, generate, init, manifest, reset

__all__ = (
"generate",
Expand All @@ -10,4 +10,5 @@
"destroy",
"reset",
"clean",
"init",
)
34 changes: 33 additions & 1 deletion kpops/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from kpops.components.base_components.models.resource import Resource
from kpops.config import ENV_PREFIX, KpopsConfig
from kpops.pipeline import ComponentFilterPredicate, Pipeline, PipelineGenerator
from kpops.utils.cli_commands import init_project
from kpops.utils.gen_schema import (
SchemaScope,
gen_config_schema,
Expand Down Expand Up @@ -71,7 +72,22 @@
help="Path to YAML with pipeline definition",
)

PIPELINE_STEPS: str | None = typer.Option(
PROJECT_PATH: Path = typer.Argument(
default=...,
exists=False,
file_okay=False,
dir_okay=True,
readable=True,
resolve_path=True,
help="Path for a new KPOps project. It should lead to an empty (or non-existent) directory. The part of the path that doesn't exist will be created.",
)

CONFIG_INCLUDE_OPTIONAL: bool = typer.Option(
default=False,
help="Whether to include non-required settings in the generated 'config.yaml'",
)

PIPELINE_STEPS: Optional[str] = typer.Option(
default=None,
envvar=f"{ENV_PREFIX}PIPELINE_STEPS",
help="Comma separated list of steps to apply the command on",
Expand Down Expand Up @@ -109,6 +125,7 @@
),
)


logger = logging.getLogger()
logging.getLogger("httpx").setLevel(logging.WARNING)
stream_handler = logging.StreamHandler()
Expand Down Expand Up @@ -189,6 +206,21 @@ def create_kpops_config(
)


@app.command( # pyright: ignore[reportCallIssue] https://github.com/rec/dtyper/issues/8
help="Initialize a new KPOps project."
)
def init(
path: Path = PROJECT_PATH,
config_include_opt: bool = CONFIG_INCLUDE_OPTIONAL,
):
if not path.exists():
path.mkdir(parents=False)
elif next(path.iterdir(), False):
log.warning("Please provide a path to an empty directory.")
return
init_project(path, config_include_opt)


@app.command( # pyright: ignore[reportCallIssue] https://github.com/rec/dtyper/issues/8
help="""
Generate JSON schema.
Expand Down
81 changes: 81 additions & 0 deletions kpops/utils/cli_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import logging
from pathlib import Path
from typing import Any

import yaml
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from pydantic_core import PydanticUndefined

from hooks.gen_docs.gen_docs_env_vars import collect_fields
from kpops.config import KpopsConfig
from kpops.utils.docstring import describe_object
from kpops.utils.json import is_jsonable
from kpops.utils.pydantic import issubclass_patched

log = logging.getLogger("cli_commands_utils")
sujuka99 marked this conversation as resolved.
Show resolved Hide resolved


def extract_config_fields_for_yaml(
fields: dict[str, Any], required: bool
) -> dict[str, Any]:
"""Return only (non-)required fields and their respective default values.

:param fields: Dict containing the fields to be categorized. The key of a
record is the name of the field, the value is the field's type.
:param required: Whether to extract only the required fields or only the
non-required ones.
"""
extracted_fields = {}
for key, value in fields.items():
if issubclass(type(value), FieldInfo):
if required and value.default in [PydanticUndefined, Ellipsis]:
extracted_fields[key] = None
elif not (required or value.default in [PydanticUndefined, Ellipsis]):
if is_jsonable(value.default):
sujuka99 marked this conversation as resolved.
Show resolved Hide resolved
extracted_fields[key] = value.default
elif issubclass_patched(value.default, BaseModel):
extracted_fields[key] = value.default.model_dump(mode="json")
else:
extracted_fields[key] = str(value.default)
else:
extracted_fields[key] = extract_config_fields_for_yaml(
fields[key], required
)
return extracted_fields


def create_config(file_name: str, dir_path: Path, include_optional: bool) -> None:
"""Create a KPOps config yaml.

:param file_name: Name for the file
:param dir_path: Directory in which the file should be created
:param include_optional: Whether to include non-required settings
"""
file_path = Path(dir_path / (file_name + ".yaml"))
file_path.touch(exist_ok=False)
with file_path.open(mode="w") as conf:
conf.write("# " + describe_object(KpopsConfig.__doc__)) # Write title
non_required = extract_config_fields_for_yaml(
collect_fields(KpopsConfig), False
)
required = extract_config_fields_for_yaml(collect_fields(KpopsConfig), True)
for k in non_required:
required.pop(k, None)
conf.write("\n\n# Required fields\n")
conf.write(yaml.dump(required))
if include_optional:
conf.write("\n# Non-required fields\n")
conf.write(yaml.dump(non_required))


def init_project(path: Path, conf_incl_opt: bool):
"""Initiate a default empty project.

:param path: Directory in which the project should be initiated
:param conf_incl_opt: Whether to include non-required settings
in the generated config file
"""
create_config("config", path, conf_incl_opt)
Path(path / ("pipeline.yaml")).touch(exist_ok=False)
sujuka99 marked this conversation as resolved.
Show resolved Hide resolved
Path(path / ("defaults.yaml")).touch(exist_ok=False)
sujuka99 marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions kpops/utils/docstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def _trim_description_end(desc: str) -> str:
desc_enders = [
":param ",
":returns:",
":raises:",
"defaults to ",
]
end_index = len(desc)
Expand Down
15 changes: 15 additions & 0 deletions kpops/utils/json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import json
from typing import Any


def is_jsonable(input: Any) -> bool:
"""Check whether a value is json-serializable.

:param input: Value to be checked.
"""
try:
json.dumps(input)
except (TypeError, OverflowError):
return False
else:
return True
4 changes: 2 additions & 2 deletions kpops/utils/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def issubclass_patched(
issubclass(BaseSettings, BaseModel) # True
issubclass(set[str], BaseModel) # raises Exception

:param cls: class to check
:base: class(es) to check against, defaults to ``BaseModel``
:param __cls: class to check
:param __class_or_tuple: class(es) to check against, defaults to ``BaseModel``
:return: Whether 'cls' is derived from another class or is the same class.
"""
try:
Expand Down
11 changes: 11 additions & 0 deletions kpops/utils/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
log = logging.getLogger("Yaml")


def touch_yaml_file(file_name, dir_path) -> Path:
sujuka99 marked this conversation as resolved.
Show resolved Hide resolved
sujuka99 marked this conversation as resolved.
Show resolved Hide resolved
"""Create an empty YAML file.

:param file_name: Name of the new file
:param dir_path: Parent directory of the new file
"""
file_path = Path(dir_path / (file_name + ".yaml"))
sujuka99 marked this conversation as resolved.
Show resolved Hide resolved
file_path.touch(exist_ok=False)
return file_path


def generate_hashkey(
file_path: Path, substitution: Mapping[str, Any] | None = None
) -> tuple:
Expand Down
40 changes: 40 additions & 0 deletions tests/utils/test_cli_commands.py
disrupted marked this conversation as resolved.
Show resolved Hide resolved
sujuka99 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from pathlib import Path

from typer.testing import CliRunner

import kpops
from kpops.cli.main import app
from kpops.utils.cli_commands import create_config

runner = CliRunner()


def test_create_config(tmp_path: Path):
opt_conf_name = "config_with_non_required"
req_conf_name = "config_with_only_required"
create_config(opt_conf_name, tmp_path, True)
create_config(req_conf_name, tmp_path, False)
assert (opt_conf := Path(tmp_path / (opt_conf_name + ".yaml"))).exists()
assert (req_conf := Path(tmp_path / (req_conf_name + ".yaml"))).exists()
with opt_conf.open() as opt_file, req_conf.open() as req_file:
assert len(opt_file.readlines()) > len(req_file.readlines())


def test_init_project(tmp_path: Path):
kpops.init(tmp_path, config_include_opt=True)
for path in ["config.yaml", "defaults.yaml", "pipeline.yaml"]:
assert Path(tmp_path / path).exists()


def test_init_project_from_cli_with_bad_path(tmp_path: Path):
bad_path = Path(tmp_path / "random_file.yaml")
bad_path.touch()
result = runner.invoke(
app,
[
"init",
str(bad_path),
],
catch_exceptions=False,
)
assert result.exit_code == 2
Loading