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

Issue:
Making fractal client less verbose by default
Currently, using the example scripts is very verbose. We get all of the debug messages as well as the full workflow after every task that was added with `fractal workflow add-task`.

Can we make the client less verbose by default? e.g. by default if only returns INFO & above (and if a user adds a -vv flag, it also gives the debug => this already seems possible according to the help message of the client). Would this change if I mess with the logging level server side, i.e. if I decrease/increase this, then would that affect whether the client prints DEBUG messages by default? Or is this a client-side setting?

And can we turn off the default printing of the full workflow after every `fractal workflow add-task`? The example scripts contain a `fractal workflow show $WF_ID`, which would then show the final output once. Currently, when adding 8 tasks, I get 8 (increasingly long) workflows printed to the console.


Discussion / hints:

Fix patch:
diff --git a/fractal/client.py b/fractal/client.py
index 1973f3b13..32f2a5012 100644
--- a/fractal/client.py
+++ b/fractal/client.py
@@ -28,12 +28,16 @@
 from .parser import parser_main
 
 
-logging.basicConfig(level=logging.DEBUG)
-
-
 async def handle(cli_args: List[str] = argv):
     args = parser_main.parse_args(cli_args[1:])
-    logging.debug(args)
+
+    # Set logging level
+    if args.verbose:
+        logging.basicConfig(level=logging.DEBUG)
+    else:
+        logging.basicConfig(level=settings.FRACTAL_LOGGING_LEVEL)
+
+    logging.debug(f"Arguments: {args}")
 
     if args.cmd:
         handler = getattr(cmd, args.cmd)
diff --git a/fractal/cmd/__init__.py b/fractal/cmd/__init__.py
index 600799354..a7b87779c 100644
--- a/fractal/cmd/__init__.py
+++ b/fractal/cmd/__init__.py
@@ -161,7 +161,7 @@ async def workflow(
     elif subcmd == "delete":
         iface = await workflow_delete(client, **kwargs)
     elif subcmd == "add-task":
-        iface = await workflow_add_task(client, **kwargs)
+        iface = await workflow_add_task(client, batch=batch, **kwargs)
     elif subcmd == "edit-task":
         iface = await workflow_edit_task(client, **kwargs)
     elif subcmd == "rm-task":
diff --git a/fractal/cmd/_task.py b/fractal/cmd/_task.py
index a969f1b77..d031d2144 100644
--- a/fractal/cmd/_task.py
+++ b/fractal/cmd/_task.py
@@ -53,10 +53,10 @@ async def task_collect_pip(
 
 
 async def task_collection_check(
-    client: AuthClient, *, state_id: int, verbose: bool, **kwargs
+    client: AuthClient, *, state_id: int, include_logs: bool, **kwargs
 ) -> BaseInterface:
     res = await client.get(
-        f"{settings.BASE_URL}/task/collect/{state_id}?verbose={verbose}"
+        f"{settings.BASE_URL}/task/collect/{state_id}?verbose={include_logs}"
     )
     state = check_response(res, expected_status_code=200, coerce=StateRead)
     return RichJsonInterface(retcode=0, data=state.sanitised_dict())
diff --git a/fractal/cmd/_workflow.py b/fractal/cmd/_workflow.py
index 4e5768f87..5dd977e22 100644
--- a/fractal/cmd/_workflow.py
+++ b/fractal/cmd/_workflow.py
@@ -114,6 +114,7 @@ async def workflow_add_task(
     client: AuthClient,
     *,
     id: int,
+    batch: bool = False,
     task_id_or_name: str,
     order: int = None,
     args_file: Optional[str] = None,
@@ -144,7 +145,12 @@ async def workflow_add_task(
         res, expected_status_code=201, coerce=WorkflowRead
     )
 
-    return RichJsonInterface(retcode=0, data=workflow.dict())
+    if batch:
+        return PrintInterface(
+            retcode=0, data=f"Added task {task_id} to workflow {workflow.id}"
+        )
+    else:
+        return RichJsonInterface(retcode=0, data=workflow.dict())
 
 
 async def workflow_edit_task(
diff --git a/fractal/config.py b/fractal/config.py
index b075262e7..d33c733a6 100644
--- a/fractal/config.py
+++ b/fractal/config.py
@@ -10,6 +10,7 @@
 Institute for Biomedical Research and Pelkmans Lab from the University of
 Zurich.
 """
+import logging
 from os import getenv
 
 from dotenv import load_dotenv
@@ -32,6 +33,8 @@ class Settings(BaseSettings):
     PROJECT_NAME: str = "Fractal client"
     PROJECT_VERSION: str = __VERSION__
 
+    FRACTAL_LOGGING_LEVEL: int = logging.INFO
+
     FRACTAL_USER: str = fail_getenv("FRACTAL_USER")
     FRACTAL_PASSWORD: str = fail_getenv("FRACTAL_PASSWORD")
     SLURM_USER: str = fail_getenv("SLURM_USER")
diff --git a/fractal/parser.py b/fractal/parser.py
index 2b9b5635e..7e53538ae 100644
--- a/fractal/parser.py
+++ b/fractal/parser.py
@@ -15,7 +15,7 @@
 import argparse as ap
 
 
-parser_main = ap.ArgumentParser(description="Fractal Analytics Framework")
+parser_main = ap.ArgumentParser(description="Client component of Fractal")
 
 parser_main.add_argument(
     "-u",
@@ -27,10 +27,11 @@
 )
 parser_main.add_argument("-s", "--slurm_user", help="Slurm user")
 parser_main.add_argument(
+    "--verbose",
     "-v",
-    action="count",
-    default=0,
-    help="Use one or more time to increase verbosity level",
+    action="store_true",
+    default=False,
+    help="Change minimal logging level from default to DEBUG",
 )
 parser_main.add_argument(
     "--batch",
@@ -250,10 +251,10 @@
     help="State ID of the collection (see output of task collect)",
 )
 task_check_collection_parser.add_argument(
-    "--verbose",
+    "--include-logs",
     default=False,
     action="store_true",
-    help="Output more verbose output",
+    help="Also include task-collection logs",
 )
 
 
@@ -495,5 +496,5 @@
 
 # VERSION GROUP
 version_parser = subparsers_main.add_parser(
-    "version", help="Print verison and exit"
+    "version", help="Print version and exit"
 )


Test patch:
diff --git a/tests/test_task.py b/tests/test_task.py
index 335619026..473f4a286 100644
--- a/tests/test_task.py
+++ b/tests/test_task.py
@@ -11,10 +11,11 @@
 async def test_task_collection_and_list(register_user, invoke, testdata_path):
     """
     GIVEN a pip installable package containing fractal-compatible tasks
+
     WHEN the collection subcommand is called
     THEN
         * the collection is initiated in the background
-        * the server returns immediately and the client displays the
+        * the server returns immediately
 
     WHEN the list command is called
     THEN the tasks collected are shown
@@ -34,15 +35,19 @@ async def test_task_collection_and_list(register_user, invoke, testdata_path):
     starting_time = time.perf_counter()
     while True:
         res1 = await invoke(f"task check-collection {state_id}")
+        assert res1.retcode == 0
         res1.show()
         await asyncio.sleep(1)
         if res1.data["data"]["status"] == "OK":
             break
         assert time.perf_counter() - starting_time < COLLECTION_TIMEOUT
+    assert res1.data["data"]["log"] is None
 
-    res2 = await invoke(f"task check-collection {state_id} --verbose")
-    debug(res2)
+    # Add --include-logs flag
+    res2 = await invoke(f"task check-collection {state_id} --include-logs")
+    assert res2.retcode == 0
     res2.show()
+    assert res2.data["data"]["log"] is not None
     assert res2.data["data"]["status"] == "OK"
 
     # LIST
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index a35cb79b8..0a56e644c 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -112,6 +112,15 @@ async def test_add_task(
     assert res.data["task_list"][0]["args"] == ARGS
     assert res.data["task_list"][0]["meta"] == META
 
+    cmd = f"--batch workflow add-task {wf.id} {t.id}"
+    debug(cmd)
+    res = await invoke(cmd)
+    assert res.retcode == 0
+    debug(res.data)
+    # Note: this check will change if we include some actual information (e.g.
+    # a WorkflowTask id) in the --batch case
+    assert res.data.startswith("Added")
+
 
 async def test_add_task_by_name(
     invoke,