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

Issue:
Expose `args_schema` and `args_schema_version` in `task {new,edit}`
This first requires:
* https://github.com/fractal-analytics-platform/fractal-server/issues/737


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5c77f263..379505173 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,8 @@
     * Add `--version` to `fractal task {new,edit}` subcommands (\#493).
     * Split `task-id-or-name` argument of `task edit` and `workflow add-task` into two arguments (\#504).
     * Add `--pinned-dependency` argument to `task collect` (\#508).
+    * Add `--args-schema` and `--args-schema-version` arguments to `task new` command (\#511).
+    * Add `--new-args-schema` and `--new-args-schema-version` arguments to `task edit` command (\#511).
 * Package and repository:
     * Fix a bug in tests, by starting the fractal-server FastAPI app in a more standard way (\#481).
     * Require pydantic version to be `>=1.10.8` (\#486, \#490).
diff --git a/fractal/cmd/__init__.py b/fractal/cmd/__init__.py
index df0a2abdb..92dac5964 100644
--- a/fractal/cmd/__init__.py
+++ b/fractal/cmd/__init__.py
@@ -164,6 +164,8 @@ async def task(
             "output_type",
             "version",
             "meta_file",
+            "args_schema",
+            "args_schema_version",
         ]
         function_kwargs = get_kwargs(parameters, kwargs)
         iface = await post_task(client, batch=batch, **function_kwargs)
@@ -178,6 +180,8 @@ async def task(
             "new_output_type",
             "new_version",
             "meta_file",
+            "new_args_schema",
+            "new_args_schema_version",
         ]
         function_kwargs = get_kwargs(parameters, kwargs)
         iface = await patch_task(client, **function_kwargs)
diff --git a/fractal/cmd/_task.py b/fractal/cmd/_task.py
index 73b4d2f42..07fea1776 100644
--- a/fractal/cmd/_task.py
+++ b/fractal/cmd/_task.py
@@ -110,9 +110,11 @@ async def post_task(
     source: str,
     input_type: str = "Any",
     output_type: str = "Any",
+    batch: bool = False,
     version: Optional[str] = None,
     meta_file: Optional[str] = None,
-    batch: bool = False,
+    args_schema: Optional[str] = None,
+    args_schema_version: Optional[str] = None,
 ) -> BaseInterface:
     optionals = {}
     if version:
@@ -120,6 +122,12 @@ async def post_task(
     if meta_file:
         with open(meta_file, "r") as f:
             optionals["meta"] = json.load(f)
+    if args_schema:
+        with open(args_schema, "r") as f:
+            optionals["args_schema"] = json.load(f)
+    if args_schema_version:
+        optionals["args_schema_version"] = args_schema_version
+
     payload = TaskCreate(
         name=name,
         command=command,
@@ -149,6 +157,8 @@ async def patch_task(
     new_output_type: Optional[str] = None,
     new_version: Optional[str] = None,
     meta_file: Optional[str] = None,
+    new_args_schema: Optional[str] = None,
+    new_args_schema_version: Optional[str] = None,
 ) -> BaseInterface:
 
     if id:
@@ -180,6 +190,11 @@ async def patch_task(
     if meta_file:
         with open(meta_file, "r") as f:
             update["meta"] = json.load(f)
+    if new_args_schema:
+        with open(new_args_schema, "r") as f:
+            update["args_schema"] = json.load(f)
+    if new_args_schema_version:
+        update["args_schema_version"] = new_args_schema_version
 
     task_update = TaskUpdate(**update)
     payload = task_update.dict(exclude_unset=True)
diff --git a/fractal/parser.py b/fractal/parser.py
index 4b83b0038..b81b8c408 100644
--- a/fractal/parser.py
+++ b/fractal/parser.py
@@ -370,6 +370,19 @@
     help="Path to JSON file with additional parameters useful for execution",
     type=str,
 )
+task_new_parser.add_argument(
+    "--args-schema",
+    help="Path to file containing JSON Schema for task arguments",
+    type=str,
+)
+task_new_parser.add_argument(
+    "--args-schema-version",
+    help=(
+        "Label encoding how the task-arguments JSON Schema was generated "
+        "(e.g. `pydantic_v1`)."
+    ),
+    type=str,
+)
 
 # task edit
 task_edit_parser = task_subparsers.add_parser(
@@ -424,7 +437,18 @@
     type=str,
     help="New version",
 )
-
+task_edit_parser.add_argument(
+    "--new-args-schema",
+    help="Path to file containing the new JSON Schema for task arguments",
+    type=str,
+)
+task_edit_parser.add_argument(
+    "--new-args-schema-version",
+    help=(
+        "New label encoding how the task-arguments JSON Schema was generated."
+    ),
+    type=str,
+)
 
 # task delete
 task_delete_parser = task_subparsers.add_parser(
diff --git a/poetry.lock b/poetry.lock
index 01792251a..443354ce3 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -600,7 +600,7 @@ sqlalchemy = ["fastapi-users-db-sqlalchemy (>=4.0.0)"]
 
 [[package]]
 name = "fractal-server"
-version = "1.3.0a4"
+version = "1.3.0a5"
 description = "Server component of the Fractal analytics platform"
 optional = false
 python-versions = "^3.9"
@@ -612,6 +612,7 @@ aiosqlite = "^0.17.0"
 alembic = "^1.9.1"
 fastapi = "^0.95.0"
 fastapi-users = {version = "^10.1", extras = ["oauth"]}
+greenlet = "*"
 pydantic = ">=1.10.8"
 python-dotenv = "^0.20.0"
 sqlalchemy = "^1.4"
@@ -628,7 +629,7 @@ slurm = ["cloudpickle (>=2.2.1,<2.3.0)", "clusterfutures (>=0.5,<0.6)"]
 type = "git"
 url = "https://github.com/fractal-analytics-platform/fractal-server.git"
 reference = "main"
-resolved_reference = "c75803db225d4058ed4e6d6a2f5d42ff70a505e1"
+resolved_reference = "24d125868091a0e5271d479254f8527346d98ad1"
 
 [[package]]
 name = "greenlet"


Test patch:
diff --git a/tests/test_task.py b/tests/test_task.py
index 92cac2b80..381f838e6 100644
--- a/tests/test_task.py
+++ b/tests/test_task.py
@@ -133,10 +133,19 @@ async def test_repeated_task_collection(register_user, invoke, testdata_path):
     assert res0.data["data"]["info"] == "Already installed"
 
 
-async def test_task_new(register_user, invoke):
+async def test_task_new(register_user, invoke, tmp_path):
 
     # create a new task with just positional required args
-    res = await invoke("task new _name _command _source --version _version")
+
+    schema_path = str(tmp_path / "schema.json")
+    schema = dict(key1=1, key2=[2, "3"])
+    with open(schema_path, "w") as f:
+        json.dump(schema, f)
+
+    res = await invoke(
+        "task new _name _command _source --version _version "
+        f"--args-schema {schema_path} --args-schema-version 1.0.0"
+    )
     res.show()
     assert res.retcode == 0
     assert res.data["name"] == "_name"
@@ -145,8 +154,8 @@ async def test_task_new(register_user, invoke):
     assert res.data["input_type"] == res.data["output_type"] == "Any"
     assert res.data["version"] == "_version"
     assert res.data["meta"] == {}
-    assert res.data["args_schema"] is None
-    assert res.data["args_schema_version"] is None
+    assert res.data["args_schema"] == schema
+    assert res.data["args_schema_version"] == "1.0.0"
 
     assert "owner" in res.data.keys()
     first_task_id = int(res.data["id"])
@@ -173,9 +182,18 @@ async def test_task_edit(
     register_user,
     invoke,
     invoke_as_superuser,
+    tmp_path,
     testdata_path: Path,
 ):
-    task = await invoke("task new _name _command _source")
+    schema_path = str(tmp_path / "schema.json")
+    schema = dict(key1=1, key2=[2, "3"])
+    with open(schema_path, "w") as f:
+        json.dump(schema, f)
+
+    task = await invoke(
+        "task new _name _command _source "
+        f"--args-schema {schema_path} --args-schema-version 1.0.0"
+    )
     task.show()
     assert task.retcode == 0
     task_id = task.data["id"]
@@ -282,6 +300,19 @@ async def test_task_edit(
     debug(res.data)
     assert res.retcode == 0
 
+    # Test succesful edit of args_schema (and args_schema_version)
+    new_schema_path = str(tmp_path / "new_schema.json")
+    new_schema = dict(key1=159, key3=None)
+    with open(new_schema_path, "w") as f:
+        json.dump(new_schema, f)
+    res = await invoke_as_superuser(
+        f"task edit --id {task_id} --new-args-schema {new_schema_path} "
+        "--new-args-schema-version 1.2.3"
+    )
+    assert res.retcode == 0
+    assert res.data["args_schema"] == new_schema
+    assert res.data["args_schema_version"] == "1.2.3"
+
 
 async def test_task_delete(
     register_user, user_factory, invoke, invoke_as_superuser