Skip to content

Commit

Permalink
Merge branch 'main' into cli-dataset-cmds
Browse files Browse the repository at this point in the history
  • Loading branch information
leoschwarz committed Feb 13, 2025
2 parents 5dd4a2b + d15a494 commit 56f6d5e
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 7 deletions.
4 changes: 4 additions & 0 deletions bfabric_app_runner/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- Implement `--force-storage` to pass a yaml to a forced storage instead of the real one.

### Changed

- Resolve workunit_ref to absolute path if it is a Path instance for CLI.

## \[0.0.15\] - 2025-02-06

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

import yaml
from pydantic import ValidationError

from bfabric.experimental.workunit_definition import WorkunitDefinition
from bfabric_app_runner.specs.app.app_spec import AppSpec
from bfabric_app_runner.specs.app.app_version import AppVersion
from bfabric.experimental.workunit_definition import WorkunitDefinition

if TYPE_CHECKING:
from pathlib import Path
from bfabric import Bfabric


Expand Down Expand Up @@ -57,6 +57,8 @@ def load_workunit_information(
steps to avoid unnecessary B-Fabric lookups. (If the workunit_ref was already a path, it will be returned as is,
otherwise the file will be created in the work directory.)
"""
if isinstance(workunit_ref, Path):
workunit_ref = workunit_ref.resolve()
workunit_definition_file = work_dir / "workunit_definition.yml"
workunit_definition = WorkunitDefinition.from_ref(workunit_ref, client, cache_file=workunit_definition_file)
app_parsed = _load_spec(
Expand Down
6 changes: 6 additions & 0 deletions bfabric_scripts/doc/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Versioning currently follows `X.Y.Z` where

- Add missing default value for columns in `bfabric-cli api read`

### Added

- `bfabric-cli api update` command to update an existing entity
- `bfabric-cli api create` command to create a new entity
- `bfabric-cli api delete` command to delete an existing entity

## \[1.13.20\] - 2025-02-10

### Added
Expand Down
37 changes: 37 additions & 0 deletions bfabric_scripts/src/bfabric_scripts/cli/api/cli_api_create.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import cyclopts
from cyclopts import Parameter
from loguru import logger
from pydantic import BaseModel, field_validator, Field, ValidationError
from rich.pretty import pprint

from bfabric import Bfabric
from bfabric.utils.cli_integration import use_client

app = cyclopts.App()


@Parameter(name="*")
class Params(BaseModel):
endpoint: str
"""Endpoint to update, e.g. 'resource'."""
attributes: list[tuple[str, str]] | None = Field(min_length=1)
"""List of attribute-value pairs to update the entity with."""

@field_validator("attributes")
def _must_not_contain_id(cls, value):
if value:
for attribute, _ in value:
if attribute == "id":
raise ValueError("Attribute 'id' is not allowed in the attributes.")
return value


@app.default
@use_client
@logger.catch(reraise=True)
def create(params: Params, *, client: Bfabric) -> None:
"""Creates a new entity in B-Fabric."""
attributes_dict = {attribute: value for attribute, value in params.attributes}
result = client.save(params.endpoint, attributes_dict)
logger.info(f"{params.endpoint} entity with ID {result[0]["id"]} created successfully.")
pprint(result)
55 changes: 55 additions & 0 deletions bfabric_scripts/src/bfabric_scripts/cli/api/cli_api_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import cyclopts
import rich
from loguru import logger
from pydantic import BaseModel
from rich.panel import Panel
from rich.pretty import Pretty
from rich.prompt import Confirm

from bfabric import Bfabric
from bfabric.utils.cli_integration import use_client

app = cyclopts.App()


@cyclopts.Parameter(name="*")
class Params(BaseModel):
endpoint: str
"""The endpoint to delete from, e.g. 'resource'."""

id: list[int]
"""The id or ids to delete."""

no_confirm: bool = False
"""Whether to ask for confirmation before deleting."""


def _perform_delete(client: Bfabric, endpoint: str, id: list[int]) -> None:
"""Deletes the entity with the given id from the given endpoint."""
result = client.delete(endpoint=endpoint, id=id).to_list_dict()
logger.info(f"Entity with ID(s) {id} deleted successfully.")


@app.default
@logger.catch(reraise=True)
@use_client
def cli_api_delete(params: Params, *, client: Bfabric) -> None:
"""Deletes entities from B-Fabric by id."""
if params.no_confirm:
_perform_delete(client=client, endpoint=params.endpoint, id=params.id)
else:
existing_entities = client.read(params.endpoint, {"id": params.id})

for id in params.id:
existing_entity = next((e for e in existing_entities if e["id"] == id), None)
if not existing_entity:
logger.warning(f"No entity found with ID {id}")
continue

rich.print(
Panel.fit(
Pretty(existing_entity, expand_all=False), title=f"{params.endpoint.capitalize()} with ID {id}"
)
)
if Confirm.ask(f"Delete entity with ID {id}?"):
_perform_delete(client=client, endpoint=params.endpoint, id=[id])
2 changes: 1 addition & 1 deletion bfabric_scripts/src/bfabric_scripts/cli/api/cli_api_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from bfabric import Bfabric
from bfabric.utils.cli_integration import use_client

cmd = cyclopts.App(help="write log messages of external jobs")
cmd = cyclopts.App(help="DEPRECATED")

log_target = cyclopts.Group(
"Log Target",
Expand Down
4 changes: 2 additions & 2 deletions bfabric_scripts/src/bfabric_scripts/cli/api/cli_api_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ def render_output(results: list[dict[str, Any]], params: Params, client: Bfabric

@app.default
@use_client
@logger.catch()
@logger.catch(reraise=True)
def read(params: Annotated[Params, cyclopts.Parameter(name="*")], *, client: Bfabric) -> None | int:
"""Reads one type of entity from B-Fabric."""
"""Reads entities from B-Fabric."""
console_user = Console(stderr=True)
console_user.print(params)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from bfabric import Bfabric
from bfabric.utils.cli_integration import use_client

app = cyclopts.App()
app = cyclopts.App(help="DEPRECATED")


@app.default
Expand Down
71 changes: 71 additions & 0 deletions bfabric_scripts/src/bfabric_scripts/cli/api/cli_api_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import cyclopts
import rich
import rich.prompt
from cyclopts import Parameter
from loguru import logger
from pydantic import BaseModel
from rich.panel import Panel
from rich.pretty import Pretty, pprint

from bfabric import Bfabric
from bfabric.utils.cli_integration import use_client

app = cyclopts.App()


@Parameter(name="*")
class Params(BaseModel):
endpoint: str
"""Endpoint to update, e.g. 'resource'."""
entity_id: int
"""ID of the entity to update."""
attributes: list[tuple[str, str]] | None = None
"""List of attribute-value pairs to update the entity with."""
no_confirm: bool = False
"""If set, the update will be performed without asking for confirmation."""


@app.default
@use_client
@logger.catch(reraise=True)
def update(params: Params, *, client: Bfabric) -> None:
"""Updates an existing entity in B-Fabric."""
attributes_dict = _sanitize_attributes(params.attributes, params.entity_id)
if not attributes_dict:
return

if not params.no_confirm:
if not _confirm_action(attributes_dict, client, params.endpoint, params.entity_id):
return

result = client.save(params.endpoint, {"id": params.entity_id, **attributes_dict})
logger.info(f"Entity with ID {params.entity_id} updated successfully.")
pprint(result)


def _confirm_action(attributes_dict: dict[str, str], client: Bfabric, endpoint: str, entity_id: int) -> bool:
logger.info(f"Updating {endpoint} entity with ID {entity_id} with attributes {attributes_dict}")
result_read = client.read(endpoint, {"id": entity_id}, max_results=1)
if not result_read:
raise ValueError(f"No entity found with ID {entity_id}")
rich.print(Panel.fit(Pretty(result_read[0], expand_all=False), title="Existing entity"))
rich.print(Panel.fit(Pretty(attributes_dict, expand_all=False), title="Updates"))
if not rich.prompt.Confirm.ask("Do you want to proceed with the update?"):
logger.info("Update cancelled by user.")
return False
return True


def _sanitize_attributes(attributes: list[tuple[str, str]] | None, entity_id: int) -> dict[str, str] | None:
if not attributes:
logger.warning("No attributes provided, doing nothing.")
return None

attributes_dict = {attribute: value for attribute, value in attributes}
if "id" in attributes_dict:
if int(attributes_dict["id"]) == entity_id:
logger.warning("Attribute 'id' is not allowed in the attributes, removing it.")
del attributes_dict["id"]
else:
raise ValueError("Attribute 'id' must match the entity_id")
return attributes_dict
11 changes: 10 additions & 1 deletion bfabric_scripts/src/bfabric_scripts/cli/cli_api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import cyclopts

from bfabric_scripts.cli.api.cli_api_create import app as _cmd_create
from bfabric_scripts.cli.api.cli_api_delete import app as _cmd_delete
from bfabric_scripts.cli.api.cli_api_log import cmd as _cmd_log
from bfabric_scripts.cli.api.cli_api_read import app as _cmd_read
from bfabric_scripts.cli.api.cli_api_save import app as _cmd_save
from bfabric_scripts.cli.api.cli_api_update import app as _cmd_update

app = cyclopts.App()
app.command(_cmd_log, name="log")
app.command(_cmd_create, name="create")
app.command(_cmd_delete, name="delete")
app.command(_cmd_read, name="read")
app.command(_cmd_update, name="update")

# TODO delete or move
app.command(_cmd_log, name="log")
# TODO delete
app.command(_cmd_save, name="save")

0 comments on commit 56f6d5e

Please sign in to comment.