Repository: fractal-analytics-platform/fractal-client (language: python)

Issue:
Add project-update command
Requires:
* https://github.com/fractal-analytics-platform/fractal-common/issues/33


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b6996aed7..ebbc2b862 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,15 +2,16 @@
 
 # 1.1.0
 
-* Remove `fastapi-users` from main dependencies (\#455); note that it remains part of the `dev` dependencies, via `fractal-server`.
-* Improve validation of API request payloads (\#447).
 * Align with fractal-server 1.1.0 (\#446).
-* Update `_TaskBase` schema from `fractal-common` (\#431).
+* Improve validation of API request payloads (\#447).
 * Drop support for python 3.8 (\#438).
+* Update `_TaskBase` schema from `fractal-common` (\#431).
+* Update `DatasetUpdate` schema (\#461).
 * Update `fractal task edit` command (\#439).
-* Disable `argparse` abbreviation for CLI commands (\#441).
+* Add `fractal project edit` command (\#465).
 * Improve task-collection log formatting (\#443).
-* Update `DatasetUpdate` schema (\#461).
+* Disable `argparse` abbreviation for CLI commands (\#441).
+* Remove `fastapi-users` from main dependencies (\#455); note that it remains part of the `dev` dependencies, via `fractal-server`.
 
 # 1.0.5
 
diff --git a/fractal/cmd/__init__.py b/fractal/cmd/__init__.py
index 6b6a3d124..221dac48a 100644
--- a/fractal/cmd/__init__.py
+++ b/fractal/cmd/__init__.py
@@ -16,6 +16,7 @@
 from ._project import project_add_dataset
 from ._project import project_create
 from ._project import project_delete
+from ._project import project_edit
 from ._project import project_list
 from ._project import project_show
 from ._task import task_collect_pip
@@ -56,6 +57,8 @@ async def project(
         iface = await project_show(client, **kwargs)
     elif subcmd == "list":
         iface = await project_list(client, **kwargs)
+    elif subcmd == "edit":
+        iface = await project_edit(client, **kwargs)
     elif subcmd == "add-dataset":
         iface = await project_add_dataset(
             client,
diff --git a/fractal/cmd/_project.py b/fractal/cmd/_project.py
index 0483e53ca..84c6eac04 100644
--- a/fractal/cmd/_project.py
+++ b/fractal/cmd/_project.py
@@ -1,6 +1,7 @@
 import json
 import logging
 from typing import Optional
+from typing import Union
 
 from rich.table import Table
 
@@ -9,6 +10,7 @@
 from ..common.schemas import DatasetRead
 from ..common.schemas import ProjectCreate
 from ..common.schemas import ProjectRead
+from ..common.schemas import ProjectUpdate
 from ..config import settings
 from ..interface import BaseInterface
 from ..interface import PrintInterface
@@ -136,3 +138,37 @@ async def project_delete(
     )
     check_response(res, expected_status_code=204)
     return PrintInterface(retcode=0, data="")
+
+
+async def project_edit(
+    client: AuthClient,
+    project_id: int,
+    new_name: Optional[str] = None,
+    new_project_dir: Optional[str] = None,
+    make_read_only: bool = False,
+    remove_read_only: bool = False,
+    **kwargs,
+) -> Union[RichJsonInterface, PrintInterface]:
+    update = {}
+    if new_name:
+        update["name"] = new_name
+    if new_project_dir:
+        update["project_dir"] = new_project_dir
+    if make_read_only:
+        update["read_only"] = True
+    if remove_read_only:
+        update["read_only"] = False
+
+    project_update = ProjectUpdate(**update)  # validation
+    payload = project_update.dict(exclude_unset=True)
+
+    if not payload:
+        return PrintInterface(retcode=1, data="Nothing to update")
+
+    res = await client.patch(
+        f"{settings.BASE_URL}/project/{project_id}", json=payload
+    )
+    new_project = check_response(
+        res, expected_status_code=200, coerce=ProjectRead
+    )
+    return RichJsonInterface(retcode=0, data=new_project.dict())
diff --git a/fractal/common b/fractal/common
index 5459d8135..e6a7ac08d 160000
--- a/fractal/common
+++ b/fractal/common
@@ -1,1 +1,1 @@
-Subproject commit 5459d81353ebbd7c8da2b6f35dbef4b130232f1a
+Subproject commit e6a7ac08d1c434eda220e1ac104efabf55bb9c74
diff --git a/fractal/parser.py b/fractal/parser.py
index 062a7c480..3564af128 100644
--- a/fractal/parser.py
+++ b/fractal/parser.py
@@ -126,6 +126,37 @@
     help="Path to file containing dataset metadata in JSON format.",
 )
 
+# project edit
+project_edit_parser = project_subparsers.add_parser(
+    "edit",
+    description="Edit details of a single project",
+    allow_abbrev=False,
+)
+project_edit_parser.add_argument(
+    "project_id", type=int, help="ID of the project to edit"
+)
+project_edit_parser.add_argument(
+    "--new-name", help="New project name", type=str, required=False
+)
+project_edit_parser.add_argument(
+    "--new-project-dir", help="New project directory", type=str, required=False
+)
+project_edit_parser_read_only = (
+    project_edit_parser.add_mutually_exclusive_group()
+)
+project_edit_parser_read_only.add_argument(
+    "--make-read-only",
+    help="Set the read-only flag for this project",
+    action="store_true",
+    required=False,
+)
+project_edit_parser_read_only.add_argument(
+    "--remove-read-only",
+    help="Remove the read-only flag for this project",
+    action="store_true",
+    required=False,
+)
+
 
 # DATASET GROUP
 dataset_parser = subparsers_main.add_parser(
diff --git a/poetry.lock b/poetry.lock
index c45482e62..f2590b9eb 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -692,7 +692,7 @@ slurm = ["clusterfutures (>=0.5,<0.6)"]
 type = "git"
 url = "https://github.com/fractal-analytics-platform/fractal-server.git"
 reference = "main"
-resolved_reference = "4a905412f6d2c6fee3d84cfe38c042f339c5b9e5"
+resolved_reference = "703d8288811235bc875c871041dce1d6f9a8c514"
 
 [[package]]
 name = "greenlet"


Test patch:
diff --git a/tests/test_project.py b/tests/test_project.py
index 9cc335ca8..746d43ba8 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -74,3 +74,54 @@ async def test_add_dataset(register_user, invoke, tmp_path):
     assert res.retcode == 0
     res.show()
     assert res.data["name"] == DATASET_NAME
+
+
+@pytest.mark.parametrize("new_name", ["new_name", None])
+@pytest.mark.parametrize("new_project_dir", ["/tmp", None])
+@pytest.mark.parametrize("read_only", [True, False, None])
+async def test_edit_project(
+    register_user,
+    invoke,
+    new_name,
+    new_project_dir,
+    read_only,
+    tmp_path,
+):
+    name = "name"
+    project_dir = str(tmp_path)
+    res = await invoke(f"project new {name} {project_dir}")
+    project = res.data
+    project_id = project["id"]
+
+    cmd = f"project edit {project_id}"
+    if new_name:
+        cmd += f" --new-name {new_name}"
+    if new_project_dir:
+        cmd += f" --new-project-dir {new_project_dir}"
+    if read_only is True:
+        cmd += " --make-read-only"
+    elif read_only is False:
+        cmd += " --remove-read-only"
+
+    res = await invoke(cmd)
+    debug(res)
+
+    if (not new_name) and (not new_project_dir) and (read_only is None):
+        assert res.retcode == 1
+    else:
+        assert res.retcode == 0
+        new_project = res.data
+        if new_name:
+            assert new_project["name"] == new_name
+        else:
+            assert new_project["name"] == name
+        if new_project_dir:
+            assert new_project["project_dir"] == new_project_dir
+        else:
+            assert new_project["project_dir"] == project_dir
+        if read_only is True:
+            assert new_project["read_only"] is True
+        if read_only is False:
+            assert new_project["read_only"] is False
+        if read_only is None:
+            assert new_project["read_only"] == project["read_only"]