Skip to content

Commit da5d8e0

Browse files
ivansurifIván Surif
andauthored
Add CLI parameters for cdf data respace plan/execute (#2522)
# Description Add CLI parameters for `cdf data respace plan/execute` Define Typer-annotated arguments and options for both subcommands: - plan: csv_file (positional), --output-file/-o (default: respace_plan.json) - execute: plan_file (positional), --backup-dir/-b (required), --dry-run/-d Add RespaceMapping and RespaceMappingList data classes for future CSV input parsing (columns: sourceSpace, externalId, targetSpace). External ID is preserved — only the space changes. These classes are not yet wired into the command logic. ## Bump - [ ] Patch - [x] Skip ## Changelog ### Changed - cognite_toolkit/_cdf_tk/apps/_respace_app.py What changed: The respace_plan() and respace_execute() methods previously had no parameters. They now have full Typer-annotated CLI parameter definitions: - `respace_plan()` Added two parameters: - `csv_file`: required positional argument pointing to an existing CSV file. Typer validates that the file exists, is a file (not a directory), and resolves it to an absolute path. Help text documents the expected columns: sourceSpace, externalId, targetSpace. - `output_file`: optional named option (--output-file / -o) defaulting to respace_plan.json in the current working directory. Both parameters are forwarded to `RespaceCommand.plan()`. - `respace_execute()` Added three parameters: - `plan_file`: required positional argument pointing to an existing JSON plan file, with the same path validation as `csv_file`. - `backup_dir`: required named option (--backup-dir / -b). No exists check since the user might pass a directory that doesn't exist yet. The execute command would create it during execution. - `dry_run`: optional boolean flag (--dry-run / -d) defaulting to False. All three parameters are forwarded to `RespaceCommand.execute()`. - cognite_toolkit/_cdf_tk/commands/_respace.py What changed: Two additions: the command methods now accept parameters, and two new data classes were added for future CSV parsing. - `RespaceMapping`: A new Pydantic model representing a single row of the input CSV. It has three required string fields (source_space, external_id, target_space) with alias_generator=to_camel_case so camelCase CSV headers (sourceSpace, externalId, targetSpace) map to snake_case fields. A `model_validator` rejects rows where sourceSpace == targetSpace. The external ID is preserved — only the space changes. - `RespaceMappingList`: A ModelList[RespaceMapping] subclass that provides CSV schema validation and parsing via the inherited `read_csv_file()` method. Only implements the required abstract method `_get_base_model_cls()`. Not yet wired into the command logic. - `RespaceCommand.plan()`: Signature changed from plan(self) to plan(self, csv_file: Path, output_file: Path). Prints the received parameters as confirmation before the placeholder message. - `RespaceCommand.execute()`: Signature changed from execute(self) to execute(self, plan_file: Path, backup_dir: Path, dry_run: bool = False). Prints the received parameters, using the verb-swapping pattern ("Would execute" / "Executing") to indicate dry run mode, consistent with the existing codebase style. --------- Co-authored-by: Iván Surif <ivan.surif@cognite.com>
1 parent 5ab8f8c commit da5d8e0

2 files changed

Lines changed: 92 additions & 7 deletions

File tree

cognite_toolkit/_cdf_tk/apps/_respace_app.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Any
1+
from pathlib import Path
2+
from typing import Annotated, Any
23

34
import typer
45
from rich import print
@@ -20,13 +21,59 @@ def main(ctx: typer.Context) -> None:
2021
print("Use [bold yellow]cdf data respace --help[/] for more information.")
2122

2223
@staticmethod
23-
def respace_plan() -> None:
24+
def respace_plan(
25+
csv_file: Annotated[
26+
Path,
27+
typer.Argument(
28+
help="Path to CSV file with nodes to respace. Expected columns: sourceSpace, externalId, targetSpace.",
29+
exists=True,
30+
file_okay=True,
31+
dir_okay=False,
32+
resolve_path=True,
33+
),
34+
],
35+
output_file: Annotated[
36+
Path,
37+
typer.Option(
38+
"--output-file",
39+
"-o",
40+
help="Path for the output plan file.",
41+
),
42+
] = Path("respace_plan.json"),
43+
) -> None:
2444
"""Generate a respace plan by analyzing nodes and their dependencies."""
2545
cmd = RespaceCommand()
26-
cmd.run(lambda: cmd.plan())
46+
cmd.run(lambda: cmd.plan(csv_file=csv_file, output_file=output_file))
2747

2848
@staticmethod
29-
def respace_execute() -> None:
49+
def respace_execute(
50+
plan_file: Annotated[
51+
Path,
52+
typer.Argument(
53+
help="Path to the respace plan JSON generated by 'plan' command.",
54+
exists=True,
55+
file_okay=True,
56+
dir_okay=False,
57+
resolve_path=True,
58+
),
59+
],
60+
backup_dir: Annotated[
61+
Path,
62+
typer.Option(
63+
"--backup-dir",
64+
"-b",
65+
help="Directory to store backup data before migration.",
66+
),
67+
],
68+
dry_run: Annotated[
69+
bool,
70+
typer.Option(
71+
"--dry-run",
72+
"-d",
73+
help="Preview changes without executing.",
74+
),
75+
] = False,
76+
) -> None:
3077
"""Execute a respace plan, migrating nodes between spaces."""
3178
cmd = RespaceCommand()
32-
cmd.run(lambda: cmd.execute())
79+
cmd.run(lambda: cmd.execute(plan_file=plan_file, backup_dir=backup_dir, dry_run=dry_run))

cognite_toolkit/_cdf_tk/commands/_respace.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,43 @@
1+
from pathlib import Path
2+
3+
from cognite.client.utils._text import to_camel_case
4+
from pydantic import BaseModel, model_validator
15
from rich import print
26

37
from cognite_toolkit._cdf_tk.client import ToolkitClient
8+
from cognite_toolkit._cdf_tk.storageio._data_classes import ModelList
49

510
from ._base import ToolkitCommand
611

712

13+
class RespaceMapping(BaseModel, alias_generator=to_camel_case, populate_by_name=True):
14+
"""A single node respace mapping from source to target.
15+
16+
The external ID is preserved — only the space changes.
17+
18+
CSV columns: sourceSpace, externalId, targetSpace
19+
"""
20+
21+
source_space: str
22+
external_id: str
23+
target_space: str
24+
25+
@model_validator(mode="after")
26+
def _target_differs_from_source(self) -> "RespaceMapping":
27+
if self.source_space == self.target_space:
28+
raise ValueError(
29+
f"targetSpace must differ from sourceSpace, "
30+
f"but both are '{self.source_space}' for externalId '{self.external_id}'"
31+
)
32+
return self
33+
34+
35+
class RespaceMappingList(ModelList[RespaceMapping]):
36+
@classmethod
37+
def _get_base_model_cls(cls) -> type[RespaceMapping]:
38+
return RespaceMapping
39+
40+
841
class RespaceCommand(ToolkitCommand):
942
def __init__(
1043
self,
@@ -15,10 +48,15 @@ def __init__(
1548
):
1649
super().__init__(print_warning, skip_tracking, silent, client)
1750

18-
def plan(self) -> None:
51+
def plan(self, csv_file: Path, output_file: Path) -> None:
1952
"""Generate a respace plan from a CSV file."""
53+
print(f"[bold]Planning respace from:[/] {csv_file}")
54+
print(f"[bold]Output plan file:[/] {output_file}")
2055
print("[bold yellow]:construction: Work in Progress, you'll be able to plan soon! :construction:[/]")
2156

22-
def execute(self) -> None:
57+
def execute(self, plan_file: Path, backup_dir: Path, dry_run: bool = False) -> None:
2358
"""Execute a respace plan."""
59+
verb = "Would execute" if dry_run else "Executing"
60+
print(f"[bold]{verb} plan:[/] {plan_file}")
61+
print(f"[bold]Backup directory:[/] {backup_dir}")
2462
print("[bold yellow]:construction: Work in Progress, you'll be able to execute soon! :construction:[/]")

0 commit comments

Comments
 (0)