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

Issue:
Align to edit-workflow-task endpoint
See https://github.com/fractal-analytics-platform/fractal-server/issues/231
Align to edit-workflow-task endpoint
See https://github.com/fractal-analytics-platform/fractal-server/issues/231


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index a5b3d3ce5..6fef3f8cb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 .pytest_cache/
+artifacts
 __pycache__
 *.cpython-*.pyc
 *.env
diff --git a/fractal/client/cmd/__init__.py b/fractal/client/cmd/__init__.py
index abc4f4b82..c8f2a9e3b 100644
--- a/fractal/client/cmd/__init__.py
+++ b/fractal/client/cmd/__init__.py
@@ -23,6 +23,7 @@
 from ._workflow import workflow_apply
 from ._workflow import workflow_delete
 from ._workflow import workflow_edit
+from ._workflow import workflow_edit_task
 from ._workflow import workflow_list
 from ._workflow import workflow_new
 from ._workflow import workflow_remove_task
@@ -158,6 +159,8 @@ async def workflow(
         iface = await workflow_delete(client, **kwargs)
     elif subcmd == "add-task":
         iface = await workflow_add_task(client, **kwargs)
+    elif subcmd == "edit-task":
+        iface = await workflow_edit_task(client, **kwargs)
     elif subcmd == "rm-task":
         iface = await workflow_remove_task(client, **kwargs)
     elif subcmd == "apply":
diff --git a/fractal/client/cmd/_workflow.py b/fractal/client/cmd/_workflow.py
index 40fe99196..5039414b2 100644
--- a/fractal/client/cmd/_workflow.py
+++ b/fractal/client/cmd/_workflow.py
@@ -13,6 +13,7 @@
 from ..schemas import WorkflowCreate
 from ..schemas import WorkflowRead
 from ..schemas import WorkflowTaskCreate
+from ..schemas import WorkflowTaskRead
 from ..schemas import WorkflowUpdate
 
 
@@ -103,6 +104,29 @@ async def workflow_add_task(
     return RichJsonInterface(retcode=0, data=workflow_task.dict())
 
 
+async def workflow_edit_task(
+    client: AuthClient,
+    *,
+    id: int,
+    workflow_task_id: int,
+    json_file: str,
+    **kwargs,
+) -> RichJsonInterface:
+
+    with Path(json_file).open("r") as f:
+        payload = json.load(f)
+
+    res = await client.patch(
+        f"{settings.BASE_URL}/workflow/{id}/edit-task/{workflow_task_id}",
+        json=payload,
+    )
+    workflow_task = check_response(
+        res, expected_status_code=200, coerce=WorkflowTaskRead
+    )
+
+    return RichJsonInterface(retcode=0, data=workflow_task.dict())
+
+
 async def workflow_remove_task(
     client: AuthClient,
     *,
diff --git a/fractal/client/parser.py b/fractal/client/parser.py
index 9f8e70f89..ec7ad9f27 100644
--- a/fractal/client/parser.py
+++ b/fractal/client/parser.py
@@ -342,6 +342,27 @@
     ),
 )
 
+# workflow edit task
+workflow_edit_task_parser = workflow_subparsers.add_parser(
+    "edit-task", help="Edit a task within a specific workflow"
+)
+workflow_edit_task_parser.add_argument(
+    "id",
+    help="Workflow id",
+)
+workflow_edit_task_parser.add_argument(
+    "workflow_task_id",
+    help="Workflow task Id, the Id of a task inside the list of tasks",
+)
+workflow_edit_task_parser.add_argument(
+    "--json-file",
+    help=(
+        "Path to a json serialised file containing the attribute "
+        "ovverrides of the task"
+    ),
+)
+
+
 # workflow remove task
 workflow_remove_task_parser = workflow_subparsers.add_parser(
     "rm-task", help="Remove a task in a specific workflow"
diff --git a/poetry.lock b/poetry.lock
index 7fa455093..2c9f0510a 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -69,7 +69,7 @@ python-versions = ">=3.5"
 dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
 docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
 tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
-tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
+tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
 
 [[package]]
 name = "bcrypt"
@@ -299,7 +299,7 @@ sqlmodel = "*"
 
 [[package]]
 name = "fractal-server"
-version = "1.0.0a1"
+version = "0.3.2"
 description = "Server component of the Fractal analytics platform"
 category = "dev"
 optional = false
@@ -322,7 +322,7 @@ tasks-core = ["fractal-tasks-core (<1.0.0)"]
 
 [package.source]
 type = "url"
-url = "https://github.com/fractal-analytics-platform/fractal-server/releases/download/1.0.0a1/fractal_server-1.0.0a1-py3-none-any.whl"
+url = "https://github.com/fractal-analytics-platform/fractal-server/releases/download/1.0.0a2/fractal_server-1.0.0a2-py3-none-any.whl"
 [[package]]
 name = "greenlet"
 version = "2.0.1"
@@ -459,7 +459,7 @@ optional = false
 python-versions = "*"
 
 [[package]]
-name = "mako"
+name = "Mako"
 version = "1.2.3"
 description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
 category = "dev"
@@ -475,7 +475,7 @@ lingua = ["lingua"]
 testing = ["pytest"]
 
 [[package]]
-name = "markupsafe"
+name = "MarkupSafe"
 version = "2.1.1"
 description = "Safely add untrusted strings to HTML/XML markup."
 category = "dev"
@@ -507,7 +507,7 @@ bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""}
 [package.extras]
 argon2 = ["argon2-cffi (>=18.2.0)"]
 bcrypt = ["bcrypt (>=3.1.0)"]
-build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
+build_docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
 totp = ["cryptography"]
 
 [[package]]
@@ -557,7 +557,7 @@ dotenv = ["python-dotenv (>=0.10.4)"]
 email = ["email-validator (>=1.0.3)"]
 
 [[package]]
-name = "pygments"
+name = "Pygments"
 version = "2.13.0"
 description = "Pygments is a syntax highlighting package written in Python."
 category = "main"
@@ -568,7 +568,7 @@ python-versions = ">=3.6"
 plugins = ["importlib-metadata"]
 
 [[package]]
-name = "pyjwt"
+name = "PyJWT"
 version = "2.6.0"
 description = "JSON Web Token implementation in Python"
 category = "main"
@@ -711,7 +711,7 @@ optional = false
 python-versions = ">=3.7"
 
 [[package]]
-name = "sqlalchemy"
+name = "SQLAlchemy"
 version = "1.4.35"
 description = "Database Abstraction Library"
 category = "main"
@@ -726,24 +726,24 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
 aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
 asyncio = ["greenlet (!=0.4.17)"]
 asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
-mariadb-connector = ["mariadb (>=1.0.1)"]
+mariadb_connector = ["mariadb (>=1.0.1)"]
 mssql = ["pyodbc"]
-mssql-pymssql = ["pymssql"]
-mssql-pyodbc = ["pyodbc"]
+mssql_pymssql = ["pymssql"]
+mssql_pyodbc = ["pyodbc"]
 mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
 mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
-mysql-connector = ["mysql-connector-python"]
+mysql_connector = ["mysql-connector-python"]
 oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
 postgresql = ["psycopg2 (>=2.7)"]
-postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
-postgresql-pg8000 = ["pg8000 (>=1.16.6)"]
-postgresql-psycopg2binary = ["psycopg2-binary"]
-postgresql-psycopg2cffi = ["psycopg2cffi"]
+postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
+postgresql_pg8000 = ["pg8000 (>=1.16.6)"]
+postgresql_psycopg2binary = ["psycopg2-binary"]
+postgresql_psycopg2cffi = ["psycopg2cffi"]
 pymysql = ["pymysql", "pymysql (<1)"]
 sqlcipher = ["sqlcipher3_binary"]
 
 [[package]]
-name = "sqlalchemy-utils"
+name = "SQLAlchemy-Utils"
 version = "0.38.3"
 description = "Various utility functions for SQLAlchemy."
 category = "dev"
@@ -763,7 +763,7 @@ password = ["passlib (>=1.6,<2.0)"]
 pendulum = ["pendulum (>=2.0.5)"]
 phone = ["phonenumbers (>=5.9.2)"]
 test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
-test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
+test_all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (>=2.7.1)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
 timezone = ["python-dateutil"]
 url = ["furl (>=0.4.1)"]
 
@@ -860,7 +860,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8"
-content-hash = "a2580360718035285399b15817b3ecaa25fe5a6c02a0ce54608b7b95bdc385c8"
+content-hash = "e0ee788652fa57a258b2b89092b8db4417b9c9951f03c9b22302b098642da33a"
 
 [metadata.files]
 aiosqlite = [
@@ -1204,11 +1204,11 @@ makefun = [
     {file = "makefun-1.15.0-py2.py3-none-any.whl", hash = "sha256:d79319f9e71b6825ca163be0afa45cbc5b1215e682efa35b2d355a03c594279c"},
     {file = "makefun-1.15.0.tar.gz", hash = "sha256:5b110e733d94f7a49d8ac27b1e2d40f2bb0501e98c1d825e0d932d26920dd5df"},
 ]
-mako = [
+Mako = [
     {file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"},
     {file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"},
 ]
-markupsafe = [
+MarkupSafe = [
     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
     {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
@@ -1308,11 +1308,11 @@ pydantic = [
     {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
     {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
 ]
-pygments = [
+Pygments = [
     {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"},
     {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"},
 ]
-pyjwt = [
+PyJWT = [
     {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"},
     {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
 ]
@@ -1355,7 +1355,7 @@ sniffio = [
     {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
     {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
 ]
-sqlalchemy = [
+SQLAlchemy = [
     {file = "SQLAlchemy-1.4.35-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:093b3109c2747d5dc0fa4314b1caf4c7ca336d5c8c831e3cfbec06a7e861e1e6"},
     {file = "SQLAlchemy-1.4.35-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:c6fb6b9ed1d0be7fa2c90be8ad2442c14cbf84eb0709dd1afeeff1e511550041"},
     {file = "SQLAlchemy-1.4.35-cp27-cp27m-win32.whl", hash = "sha256:d38a49aa75a5759d0d118e26701d70c70a37b896379115f8386e91b0444bfa70"},
@@ -1393,7 +1393,7 @@ sqlalchemy = [
     {file = "SQLAlchemy-1.4.35-cp39-cp39-win_amd64.whl", hash = "sha256:53c7469b86a60fe2babca4f70111357e6e3d5150373bc85eb3b914356983e89a"},
     {file = "SQLAlchemy-1.4.35.tar.gz", hash = "sha256:2ffc813b01dc6473990f5e575f210ca5ac2f5465ace3908b78ffd6d20058aab5"},
 ]
-sqlalchemy-utils = [
+SQLAlchemy-Utils = [
     {file = "SQLAlchemy-Utils-0.38.3.tar.gz", hash = "sha256:9f9afba607a40455cf703adfa9846584bf26168a0c5a60a70063b70d65051f4d"},
     {file = "SQLAlchemy_Utils-0.38.3-py3-none-any.whl", hash = "sha256:5c13b5d08adfaa85f3d4e8ec09a75136216fad41346980d02974a70a77988bf9"},
 ]
diff --git a/pyproject.toml b/pyproject.toml
index f1ae46e62..1d3c40ad5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,7 @@ pytest = "^7.1.2"
 bumpver = "^2022.1118"
 pytest-asyncio = "^0.19.0"
 # fractal-server = "^1.0.0"
-fractal-server = {url = "https://github.com/fractal-analytics-platform/fractal-server/releases/download/1.0.0a1/fractal_server-1.0.0a1-py3-none-any.whl"}
+fractal-server = {url = "https://github.com/fractal-analytics-platform/fractal-server/releases/download/1.0.0a2/fractal_server-1.0.0a2-py3-none-any.whl"}
 coverage = {extras = ["toml"], version = "^6.5.0"}
 
 [build-system]


Test patch:
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index 2f387eb32..d01847e1c 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -99,3 +99,53 @@ async def test_add_task(
     assert res.retcode == 0
     debug(res.data)
     assert res.data["task_list"][0]["args"] == custom_args
+
+
+async def test_edit_workflow_task(
+    clear_db,
+    testserver,
+    invoke,
+    register_user,
+    task_factory,
+    workflow_factory,
+    tmp_path,
+):
+    """
+    GIVEN a workflow
+    WHEN the client is invoked to add a task, including custom args
+    THEN
+        the WorkflowTask is correctly registered in the db, including custom
+        gargs
+    """
+    wf = await workflow_factory()
+    t = await task_factory()
+
+    # Create task, without overriding arguments
+    cmd = f"workflow add-task {wf.id} {t.id}"
+    res = await invoke(cmd)
+    assert res.retcode == 0
+
+    # New arguments to be used
+    payload = dict(args={"some_arg": "some_value"})
+
+    json_file = tmp_path / "payload.json"
+    with json_file.open("w") as f:
+        json.dump(payload, f)
+
+    # Edit workflow task
+    debug(res.data)
+    workflow_task_id = res.data["task_list"][0]["id"]
+    cmd = (
+        f"workflow edit-task {wf.id} {workflow_task_id} "
+        f"--json-file {json_file}"
+    )
+    debug(cmd)
+    res = await invoke(cmd)
+    assert res.retcode == 0
+    assert res.data["args"] == payload["args"]
+
+    # Check that also the workflow in the db was correctly updated
+    res = await invoke(f"workflow show {wf.id}")
+    assert res.retcode == 0
+    debug(res.data)
+    assert res.data["task_list"][0]["args"] == payload["args"]