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

Issue:
Add cached task names to `fractal workflow add-task`
This is currently only available in the `fractal task` subcommands.
Add cached task names to `fractal workflow add-task`
This is currently only available in the `fractal task` subcommands.


Discussion / hints:
When using the client, it would be very convenient to select a task to add to a workflow by name & version when adding them with `fractal workflow add-task`. And would be even nicer to be have the version optional with a way to default to the newest version, unless specified otherwise.

Currently, we select them by ID. But when we have multiple versions of tasks around, I need to list them to get their IDs. And those aren't fully reproducible (depends on what order the different versions of the tasks were installed and how many versions are installed for a given server installation) => tricky for examples
When using the client, it would be very convenient to select a task to add to a workflow by name & version when adding them with `fractal workflow add-task`. And would be even nicer to be have the version optional with a way to default to the newest version, unless specified otherwise.

Currently, we select them by ID. But when we have multiple versions of tasks around, I need to list them to get their IDs. And those aren't fully reproducible (depends on what order the different versions of the tasks were installed and how many versions are installed for a given server installation) => tricky for examples

Fix patch:
diff --git a/fractal/cmd/_task.py b/fractal/cmd/_task.py
index 06f33ae34..3e9ba0fcf 100644
--- a/fractal/cmd/_task.py
+++ b/fractal/cmd/_task.py
@@ -1,6 +1,3 @@
-import json
-from pathlib import Path
-from typing import List
 from typing import Optional
 
 from ..authclient import AuthClient
@@ -13,63 +10,8 @@
 from ..interface import PrintInterface
 from ..interface import RichJsonInterface
 from ..response import check_response
-
-
-async def get_cached_task_by_name(name: str, client: AuthClient) -> int:
-
-    # Set paths
-    cache_dir = Path(f"{settings.FRACTAL_CACHE_PATH}").expanduser()
-    cache_file = cache_dir / "tasks"
-
-    # If cache is missing, create it
-    cache_up_to_date = False
-    if not cache_file.exists():
-        await refresh_task_cache(client)
-        cache_up_to_date = True
-
-    # Read cache
-    with cache_file.open("r") as f:
-        task_cache = json.load(f)
-
-    # Look for name in cache
-    # Case 1: name exists in cache
-    if name in task_cache.keys():
-        return task_cache[name]
-    # Case 2: name is missing, and cache was just updated
-    elif cache_up_to_date:
-        raise KeyError(f"Task {name} not in {cache_file}\n")
-    # Case 3: name is missing but cache may be out of date
-    else:
-        await refresh_task_cache(client)
-        with cache_file.open("r") as f:
-            task_cache = json.load(f)
-        try:
-            return task_cache[name]
-        except KeyError as e:
-            raise KeyError(f"Task {name} not in {cache_file}\n", str(e))
-
-
-async def refresh_task_cache(client: AuthClient, **kwargs) -> List[dict]:
-
-    # Get task_list
-    res = await client.get(f"{settings.BASE_URL}/task/")
-    task_list = check_response(res, expected_status_code=200)
-
-    # Refresh cache of (name,id) pairs
-    task_cache = {}
-    for task in task_list:
-        task_cache[task["name"]] = task["id"]
-
-    # Set paths and create cache folder (if needed)
-    cache_dir = Path(f"{settings.FRACTAL_CACHE_PATH}").expanduser()
-    cache_file = cache_dir / "tasks"
-    cache_dir.mkdir(parents=True, exist_ok=True)
-
-    # Write task cache
-    with cache_file.open("w") as f:
-        json.dump(task_cache, f, indent=4)
-
-    return task_list
+from .utils import get_cached_task_by_name
+from .utils import refresh_task_cache
 
 
 async def task_list(client: AuthClient, **kwargs) -> RichJsonInterface:
diff --git a/fractal/cmd/_workflow.py b/fractal/cmd/_workflow.py
index 4bd541c24..0154ebcbe 100644
--- a/fractal/cmd/_workflow.py
+++ b/fractal/cmd/_workflow.py
@@ -16,6 +16,7 @@
 from ..interface import PrintInterface
 from ..interface import RichJsonInterface
 from ..response import check_response
+from .utils import get_cached_task_by_name
 
 
 async def workflow_new(
@@ -83,11 +84,17 @@ async def workflow_add_task(
     client: AuthClient,
     *,
     id: int,
-    task_id: int,
+    task_id_or_name: str,
     order: int = None,
     args_file: Optional[str] = None,
     **kwargs,
 ) -> RichJsonInterface:
+
+    try:
+        task_id = int(task_id_or_name)
+    except ValueError:
+        task_id = await get_cached_task_by_name(task_id_or_name, client)
+
     workflow_task = WorkflowTaskCreate(task_id=task_id, order=order)
     if args_file:
         with Path(args_file).open("r") as f:
diff --git a/fractal/cmd/utils.py b/fractal/cmd/utils.py
new file mode 100644
index 000000000..42026e18a
--- /dev/null
+++ b/fractal/cmd/utils.py
@@ -0,0 +1,76 @@
+import json
+import logging
+from pathlib import Path
+from typing import List
+
+from ..authclient import AuthClient
+from ..config import settings
+from ..response import check_response
+
+
+async def get_cached_task_by_name(name: str, client: AuthClient) -> int:
+
+    # Set paths
+    cache_dir = Path(f"{settings.FRACTAL_CACHE_PATH}").expanduser()
+    cache_file = cache_dir / "tasks"
+
+    # If cache is missing, create it
+    cache_up_to_date = False
+    if not cache_file.exists():
+        await refresh_task_cache(client)
+        cache_up_to_date = True
+
+    # Read cache
+    with cache_file.open("r") as f:
+        task_cache = json.load(f)
+
+    # Look for name in cache
+    # Case 1: name exists in cache
+    if name in task_cache.keys():
+        return task_cache[name]
+    # Case 2: name is missing, and cache was just updated
+    elif cache_up_to_date:
+        raise KeyError(f'Task "{name}" not in {cache_file}\n')
+    # Case 3: name is missing but cache may be out of date
+    else:
+        await refresh_task_cache(client)
+        with cache_file.open("r") as f:
+            task_cache = json.load(f)
+        try:
+            return task_cache[name]
+        except KeyError as e:
+            raise KeyError(f'Task "{name}" not in {cache_file}\n', str(e))
+
+
+async def refresh_task_cache(client: AuthClient, **kwargs) -> List[dict]:
+
+    # Get task_list
+    res = await client.get(f"{settings.BASE_URL}/task/")
+    task_list = check_response(res, expected_status_code=200)
+
+    # Check that there are no name clashes
+    names = [task["name"] for task in task_list]
+    if len(set(names)) < len(names):
+        msg = (
+            "Cannot write task-list cache if task names are not unique "
+            "(version-based disambiguation will be added in the future).\n"
+            f"Current task list includes: {names}"
+        )
+        logging.warning(msg)
+        return task_list
+
+    # Refresh cache of (name,id) pairs
+    task_cache = {}
+    for task in task_list:
+        task_cache[task["name"]] = task["id"]
+
+    # Set paths and create cache folder (if needed)
+    cache_dir = Path(f"{settings.FRACTAL_CACHE_PATH}").expanduser()
+    cache_file = cache_dir / "tasks"
+    cache_dir.mkdir(parents=True, exist_ok=True)
+
+    # Write task cache
+    with cache_file.open("w") as f:
+        json.dump(task_cache, f, indent=4)
+
+    return task_list
diff --git a/fractal/parser.py b/fractal/parser.py
index c5ea06f65..9219a9f5a 100644
--- a/fractal/parser.py
+++ b/fractal/parser.py
@@ -339,8 +339,7 @@
     help="Workflow id",
 )
 workflow_add_task_parser.add_argument(
-    "task_id",
-    help="Task id",
+    "task_id_or_name", help="ID or name of the new task", type=str
 )
 workflow_add_task_parser.add_argument(
     "--order", help="Order of this task within the workflow's task list"
diff --git a/poetry.lock b/poetry.lock
index 7bb696cbe..93761034e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -45,7 +45,7 @@ trio = ["trio (>=0.16,<0.22)"]
 
 [[package]]
 name = "asttokens"
-version = "2.1.0"
+version = "2.2.0"
 description = "Annotate AST trees with source code positions"
 category = "dev"
 optional = false
@@ -55,7 +55,7 @@ python-versions = "*"
 six = "*"
 
 [package.extras]
-test = ["astroid (<=2.5.3)", "pytest"]
+test = ["astroid", "pytest"]
 
 [[package]]
 name = "attrs"
@@ -164,7 +164,7 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "38.0.3"
+version = "38.0.4"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "dev"
 optional = false
@@ -226,7 +226,7 @@ idna = ">=2.0.0"
 
 [[package]]
 name = "exceptiongroup"
-version = "1.0.1"
+version = "1.0.4"
 description = "Backport of PEP 654 (exception groups)"
 category = "dev"
 optional = false
@@ -299,7 +299,7 @@ sqlmodel = "*"
 
 [[package]]
 name = "fractal-server"
-version = "1.0.0a13"
+version = "1.0.0b3"
 description = "Server component of the Fractal analytics platform"
 category = "dev"
 optional = false
@@ -319,10 +319,11 @@ uvicorn = ">=0.18.2,<0.19.0"
 [package.extras]
 parsl = ["parsl[monitoring] @ git+https://github.com/fractal-analytics-platform/parsl.git@1.3.1-dev"]
 postgres = ["asyncpg (>=0.27.0,<0.28.0)", "psycopg2 (>=2.9.5,<3.0.0)"]
+slurm = ["clusterfutures (>=0.4,<0.5)"]
 
 [package.source]
 type = "url"
-url = "https://github.com/fractal-analytics-platform/fractal-server/releases/download/1.0.0a13/fractal_server-1.0.0a13-py3-none-any.whl"
+url = "https://github.com/fractal-analytics-platform/fractal-server/releases/download/1.0.0b3/fractal_server-1.0.0b3-py3-none-any.whl"
 [[package]]
 name = "greenlet"
 version = "2.0.1"
@@ -337,24 +338,24 @@ test = ["faulthandler", "objgraph", "psutil"]
 
 [[package]]
 name = "h11"
-version = "0.12.0"
+version = "0.14.0"
 description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
 category = "main"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 
 [[package]]
 name = "httpcore"
-version = "0.15.0"
+version = "0.16.2"
 description = "A minimal low-level HTTP client."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 
 [package.dependencies]
-anyio = ">=3.0.0,<4.0.0"
+anyio = ">=3.0,<5.0"
 certifi = "*"
-h11 = ">=0.11,<0.13"
+h11 = ">=0.13,<0.15"
 sniffio = ">=1.0.0,<2.0.0"
 
 [package.extras]
@@ -363,7 +364,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
 
 [[package]]
 name = "httpx"
-version = "0.23.0"
+version = "0.23.1"
 description = "The next generation HTTP client."
 category = "main"
 optional = false
@@ -371,7 +372,7 @@ python-versions = ">=3.7"
 
 [package.dependencies]
 certifi = "*"
-httpcore = ">=0.15.0,<0.16.0"
+httpcore = ">=0.15.0,<0.17.0"
 rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
 sniffio = "*"
 
@@ -383,7 +384,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
 
 [[package]]
 name = "httpx-oauth"
-version = "0.10.1"
+version = "0.10.2"
 description = "Async OAuth client using HTTPX"
 category = "dev"
 optional = false
@@ -392,9 +393,6 @@ python-versions = ">=3.7"
 [package.dependencies]
 httpx = ">=0.18,<0.24"
 
-[package.extras]
-dev = ["black", "bumpversion", "codecov", "fastapi", "flake8", "flake8-docstrings", "flit", "isort", "markdown-include", "mkdocs", "mkdocs-material", "mypy", "pygments", "pymdown-extensions", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "respx"]
-
 [[package]]
 name = "idna"
 version = "3.4"
@@ -405,7 +403,7 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "importlib-metadata"
-version = "5.0.0"
+version = "5.1.0"
 description = "Read metadata from Python packages"
 category = "dev"
 optional = false
@@ -460,7 +458,7 @@ python-versions = "*"
 
 [[package]]
 name = "mako"
-version = "1.2.3"
+version = "1.2.4"
 description = "A super-fast templating language that borrows the best ideas from the existing templating languages."
 category = "dev"
 optional = false
@@ -683,7 +681,7 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
 
 [[package]]
 name = "setuptools"
-version = "65.5.1"
+version = "65.6.3"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 category = "dev"
 optional = false
@@ -847,7 +845,7 @@ standard = ["colorama (>=0.4)", "httptools (>=0.4.0)", "python-dotenv (>=0.13)",
 
 [[package]]
 name = "zipp"
-version = "3.10.0"
+version = "3.11.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "dev"
 optional = false
@@ -860,7 +858,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.8"
-content-hash = "c9120f3e98606b3fcc87dc6ec67f2d5306ebfba889542f82c96b5146211b5a6e"
+content-hash = "8fca8259227f1b459bb2242f01db504028ed096166e03edf7f29fd824db6307e"
 
 [metadata.files]
 aiosqlite = [
@@ -876,8 +874,8 @@ anyio = [
     {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"},
 ]
 asttokens = [
-    {file = "asttokens-2.1.0-py2.py3-none-any.whl", hash = "sha256:1b28ed85e254b724439afc783d4bee767f780b936c3fe8b3275332f42cf5f561"},
-    {file = "asttokens-2.1.0.tar.gz", hash = "sha256:4aa76401a151c8cc572d906aad7aea2a841780834a19d780f4321c0fe1b54635"},
+    {file = "asttokens-2.2.0-py2.py3-none-any.whl", hash = "sha256:c56caef774a929923696f09ceea0eadcb95c94b30e8ee4f9fc4f5867096caaeb"},
+    {file = "asttokens-2.2.0.tar.gz", hash = "sha256:e27b1f115daebfafd4d1826fc75f9a72f0b74bd3ae4ee4d9380406d74d35e52c"},
 ]
 attrs = [
     {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
@@ -1045,32 +1043,32 @@ coverage = [
     {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
 ]
 cryptography = [
-    {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:984fe150f350a3c91e84de405fe49e688aa6092b3525f407a18b9646f6612320"},
-    {file = "cryptography-38.0.3-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:ed7b00096790213e09eb11c97cc6e2b757f15f3d2f85833cd2d3ec3fe37c1722"},
-    {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bbf203f1a814007ce24bd4d51362991d5cb90ba0c177a9c08825f2cc304d871f"},
-    {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554bec92ee7d1e9d10ded2f7e92a5d70c1f74ba9524947c0ba0c850c7b011828"},
-    {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b52c9e5f8aa2b802d48bd693190341fae201ea51c7a167d69fc48b60e8a959"},
-    {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:728f2694fa743a996d7784a6194da430f197d5c58e2f4e278612b359f455e4a2"},
-    {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfb4f4dd568de1b6af9f4cda334adf7d72cf5bc052516e1b2608b683375dd95c"},
-    {file = "cryptography-38.0.3-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5419a127426084933076132d317911e3c6eb77568a1ce23c3ac1e12d111e61e0"},
-    {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9b24bcff7853ed18a63cfb0c2b008936a9554af24af2fb146e16d8e1aed75748"},
-    {file = "cryptography-38.0.3-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:25c1d1f19729fb09d42e06b4bf9895212292cb27bb50229f5aa64d039ab29146"},
-    {file = "cryptography-38.0.3-cp36-abi3-win32.whl", hash = "sha256:7f836217000342d448e1c9a342e9163149e45d5b5eca76a30e84503a5a96cab0"},
-    {file = "cryptography-38.0.3-cp36-abi3-win_amd64.whl", hash = "sha256:c46837ea467ed1efea562bbeb543994c2d1f6e800785bd5a2c98bc096f5cb220"},
-    {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06fc3cc7b6f6cca87bd56ec80a580c88f1da5306f505876a71c8cfa7050257dd"},
-    {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:65535bc550b70bd6271984d9863a37741352b4aad6fb1b3344a54e6950249b55"},
-    {file = "cryptography-38.0.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5e89468fbd2fcd733b5899333bc54d0d06c80e04cd23d8c6f3e0542358c6060b"},
-    {file = "cryptography-38.0.3-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6ab9516b85bebe7aa83f309bacc5f44a61eeb90d0b4ec125d2d003ce41932d36"},
-    {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:068147f32fa662c81aebab95c74679b401b12b57494872886eb5c1139250ec5d"},
-    {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:402852a0aea73833d982cabb6d0c3bb582c15483d29fb7085ef2c42bfa7e38d7"},
-    {file = "cryptography-38.0.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b1b35d9d3a65542ed2e9d90115dfd16bbc027b3f07ee3304fc83580f26e43249"},
-    {file = "cryptography-38.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6addc3b6d593cd980989261dc1cce38263c76954d758c3c94de51f1e010c9a50"},
-    {file = "cryptography-38.0.3-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:be243c7e2bfcf6cc4cb350c0d5cdf15ca6383bbcb2a8ef51d3c9411a9d4386f0"},
-    {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78cf5eefac2b52c10398a42765bfa981ce2372cbc0457e6bf9658f41ec3c41d8"},
-    {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4e269dcd9b102c5a3d72be3c45d8ce20377b8076a43cbed6f660a1afe365e436"},
-    {file = "cryptography-38.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8d41a46251bf0634e21fac50ffd643216ccecfaf3701a063257fe0b2be1b6548"},
-    {file = "cryptography-38.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:785e4056b5a8b28f05a533fab69febf5004458e20dad7e2e13a3120d8ecec75a"},
-    {file = "cryptography-38.0.3.tar.gz", hash = "sha256:bfbe6ee19615b07a98b1d2287d6a6073f734735b49ee45b11324d85efc4d5cbd"},
+    {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:2fa36a7b2cc0998a3a4d5af26ccb6273f3df133d61da2ba13b3286261e7efb70"},
+    {file = "cryptography-38.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:1f13ddda26a04c06eb57119caf27a524ccae20533729f4b1e4a69b54e07035eb"},
+    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:2ec2a8714dd005949d4019195d72abed84198d877112abb5a27740e217e0ea8d"},
+    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50a1494ed0c3f5b4d07650a68cd6ca62efe8b596ce743a5c94403e6f11bf06c1"},
+    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a10498349d4c8eab7357a8f9aa3463791292845b79597ad1b98a543686fb1ec8"},
+    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:10652dd7282de17990b88679cb82f832752c4e8237f0c714be518044269415db"},
+    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bfe6472507986613dc6cc00b3d492b2f7564b02b3b3682d25ca7f40fa3fd321b"},
+    {file = "cryptography-38.0.4-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce127dd0a6a0811c251a6cddd014d292728484e530d80e872ad9806cfb1c5b3c"},
+    {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:53049f3379ef05182864d13bb9686657659407148f901f3f1eee57a733fb4b00"},
+    {file = "cryptography-38.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:8a4b2bdb68a447fadebfd7d24855758fe2d6fecc7fed0b78d190b1af39a8e3b0"},
+    {file = "cryptography-38.0.4-cp36-abi3-win32.whl", hash = "sha256:1d7e632804a248103b60b16fb145e8df0bc60eed790ece0d12efe8cd3f3e7744"},
+    {file = "cryptography-38.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:8e45653fb97eb2f20b8c96f9cd2b3a0654d742b47d638cf2897afbd97f80fa6d"},
+    {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca57eb3ddaccd1112c18fc80abe41db443cc2e9dcb1917078e02dfa010a4f353"},
+    {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:c9e0d79ee4c56d841bd4ac6e7697c8ff3c8d6da67379057f29e66acffcd1e9a7"},
+    {file = "cryptography-38.0.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0e70da4bdff7601b0ef48e6348339e490ebfb0cbe638e083c9c41fb49f00c8bd"},
+    {file = "cryptography-38.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:998cd19189d8a747b226d24c0207fdaa1e6658a1d3f2494541cb9dfbf7dcb6d2"},
+    {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67461b5ebca2e4c2ab991733f8ab637a7265bb582f07c7c88914b5afb88cb95b"},
+    {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4eb85075437f0b1fd8cd66c688469a0c4119e0ba855e3fef86691971b887caf6"},
+    {file = "cryptography-38.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3178d46f363d4549b9a76264f41c6948752183b3f587666aff0555ac50fd7876"},
+    {file = "cryptography-38.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6391e59ebe7c62d9902c24a4d8bcbc79a68e7c4ab65863536127c8a9cd94043b"},
+    {file = "cryptography-38.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:78e47e28ddc4ace41dd38c42e6feecfdadf9c3be2af389abbfeef1ff06822285"},
+    {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fb481682873035600b5502f0015b664abc26466153fab5c6bc92c1ea69d478b"},
+    {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:4367da5705922cf7070462e964f66e4ac24162e22ab0a2e9d31f1b270dd78083"},
+    {file = "cryptography-38.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b4cad0cea995af760f82820ab4ca54e5471fc782f70a007f31531957f43e9dee"},
+    {file = "cryptography-38.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:80ca53981ceeb3241998443c4964a387771588c4e4a5d92735a493af868294f9"},
+    {file = "cryptography-38.0.4.tar.gz", hash = "sha256:175c1a818b87c9ac80bb7377f5520b7f31b3ef2a0004e2420319beadedb67290"},
 ]
 devtools = [
     {file = "devtools-0.9.0-py3-none-any.whl", hash = "sha256:689cf4e7c75024237c42093ba19f4fa9cf15980269f02463aeab4d97d4b0a215"},
@@ -1085,8 +1083,8 @@ email-validator = [
     {file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"},
 ]
 exceptiongroup = [
-    {file = "exceptiongroup-1.0.1-py3-none-any.whl", hash = "sha256:4d6c0aa6dd825810941c792f53d7b8d71da26f5e5f84f20f9508e8f2d33b140a"},
-    {file = "exceptiongroup-1.0.1.tar.gz", hash = "sha256:73866f7f842ede6cb1daa42c4af078e2035e5f7607f0e2c762cc51bb31bbe7b2"},
+    {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"},
+    {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"},
 ]
 executing = [
     {file = "executing-0.10.0-py2.py3-none-any.whl", hash = "sha256:9c745f80cda11eb22b62cbecf21156491a794eb56ab06f9d286a44e62822b24e"},
@@ -1168,28 +1166,28 @@ greenlet = [
     {file = "greenlet-2.0.1.tar.gz", hash = "sha256:42e602564460da0e8ee67cb6d7236363ee5e131aa15943b6670e44e5c2ed0f67"},
 ]
 h11 = [
-    {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
-    {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
+    {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
+    {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
 ]
 httpcore = [
-    {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"},
-    {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"},
+    {file = "httpcore-0.16.2-py3-none-any.whl", hash = "sha256:52c79095197178856724541e845f2db86d5f1527640d9254b5b8f6f6cebfdee6"},
+    {file = "httpcore-0.16.2.tar.gz", hash = "sha256:c35c5176dc82db732acfd90b581a3062c999a72305df30c0fc8fafd8e4aca068"},
 ]
 httpx = [
-    {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"},
-    {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"},
+    {file = "httpx-0.23.1-py3-none-any.whl", hash = "sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8"},
+    {file = "httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"},
 ]
 httpx-oauth = [
-    {file = "httpx-oauth-0.10.1.tar.gz", hash = "sha256:5ebd4abb8b1cc9b5b4673c1ef574fda19a52d1edead3325d38e6a65ae3c73ed6"},
-    {file = "httpx_oauth-0.10.1-py3-none-any.whl", hash = "sha256:dd3b40972016d99bd1140607f2933769808e6783763e32257e77430e02a892e6"},
+    {file = "httpx_oauth-0.10.2-py3-none-any.whl", hash = "sha256:1739531e0c1f767688b08eb6a9192f38b4d48361ec8307cc52738d61c7d77666"},
+    {file = "httpx_oauth-0.10.2.tar.gz", hash = "sha256:be92ce28fce16d443288e811fe11b71f4aab4273ee9b5e1404692addca4bca98"},
 ]
 idna = [
     {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
     {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"},
-    {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"},
+    {file = "importlib_metadata-5.1.0-py3-none-any.whl", hash = "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"},
+    {file = "importlib_metadata-5.1.0.tar.gz", hash = "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b"},
 ]
 importlib-resources = [
     {file = "importlib_resources-5.10.0-py3-none-any.whl", hash = "sha256:ee17ec648f85480d523596ce49eae8ead87d5631ae1551f913c0100b5edd3437"},
@@ -1208,8 +1206,8 @@ makefun = [
     {file = "makefun-1.15.0.tar.gz", hash = "sha256:5b110e733d94f7a49d8ac27b1e2d40f2bb0501e98c1d825e0d932d26920dd5df"},
 ]
 mako = [
-    {file = "Mako-1.2.3-py3-none-any.whl", hash = "sha256:c413a086e38cd885088d5e165305ee8eed04e8b3f8f62df343480da0a385735f"},
-    {file = "Mako-1.2.3.tar.gz", hash = "sha256:7fde96466fcfeedb0eed94f187f20b23d85e4cb41444be0e542e2c8c65c396cd"},
+    {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"},
+    {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"},
 ]
 markupsafe = [
     {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
@@ -1347,8 +1345,8 @@ rich = [
     {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"},
 ]
 setuptools = [
-    {file = "setuptools-65.5.1-py3-none-any.whl", hash = "sha256:d0b9a8433464d5800cbe05094acf5c6d52a91bfac9b52bcfc4d41382be5d5d31"},
-    {file = "setuptools-65.5.1.tar.gz", hash = "sha256:e197a19aa8ec9722928f2206f8de752def0e4c9fc6953527360d1c36d94ddb2f"},
+    {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
+    {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"},
 ]
 six = [
     {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
@@ -1429,6 +1427,6 @@ uvicorn = [
     {file = "uvicorn-0.18.3.tar.gz", hash = "sha256:9a66e7c42a2a95222f76ec24a4b754c158261c4696e683b9dadc72b590e0311b"},
 ]
 zipp = [
-    {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"},
-    {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"},
+    {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"},
+    {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"},
 ]
diff --git a/pyproject.toml b/pyproject.toml
index a66d9b1e5..d67832d7b 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.0a13/fractal_server-1.0.0a13-py3-none-any.whl"}
+fractal-server = {url = "https://github.com/fractal-analytics-platform/fractal-server/releases/download/1.0.0b3/fractal_server-1.0.0b3-py3-none-any.whl"}
 coverage = {extras = ["toml"], version = "^6.5.0"}
 
 [build-system]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 51057313c..3e7f5fc5d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,3 @@
-import os
 import shlex
 from os import environ
 from pathlib import Path
@@ -56,10 +55,12 @@ async def __invoke(args: str):
 def clear_task_cache():
     from fractal.config import settings
 
-    cache_dir = str(Path(f"{settings.FRACTAL_CACHE_PATH}").expanduser())
-    cache_file = f"{cache_dir}/tasks"
-    if os.path.isfile(cache_file):
-        os.remove(cache_file)
+    # This is a workaround to clean up the state before the test. The right way
+    # would be to inject a new (function-scoped) FRACTAL_CACHE_PATH variable
+    # for each test
+    cache_dir = Path(settings.FRACTAL_CACHE_PATH).expanduser()
+    cache_file = cache_dir / "tasks"
+    cache_file.unlink(missing_ok=True)
 
 
 from .fixtures_testserver import *  # noqa: 401
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index 69984e1ec..57635fcf4 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -1,4 +1,6 @@
 import json
+import logging
+from pathlib import Path
 
 import pytest  # noqa F401
 from devtools import debug
@@ -42,7 +44,7 @@ async def test_workflow_list(register_user, invoke):
 
 
 async def test_workflow_list_when_two_projects_exist(
-    register_user, invoke, tmp_path
+    register_user, invoke, tmp_path: Path
 ):
     res_pj1 = await invoke(f"project new PRJ1 {str(tmp_path)}/prj1")
     res_pj2 = await invoke(f"project new PRJ2 {str(tmp_path)}/prj2")
@@ -74,14 +76,14 @@ async def test_add_task(
     register_user,
     task_factory,
     workflow_factory,
-    tmp_path,
+    tmp_path: 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
+        args
     """
     wf = await workflow_factory()
     t = await task_factory()
@@ -99,12 +101,76 @@ async def test_add_task(
     assert res.data["task_list"][0]["args"] == custom_args
 
 
+async def test_add_task_by_name(
+    invoke,
+    register_user,
+    task_factory,
+    workflow_factory,
+    tmp_path: Path,
+    clear_task_cache,
+):
+    """
+    GIVEN a workflow and a task
+    WHEN the client is invoked to add a task *by name*
+    THEN the WorkflowTask is correctly registered in the db
+    """
+    wf = await workflow_factory()
+    task = await task_factory()
+    debug(task)
+
+    cmd = f"workflow add-task {wf.id} {task.name}"
+    debug(cmd)
+    res = await invoke(cmd)
+    assert res.retcode == 0
+    debug(res)
+    debug(res.data)
+    assert res.data["task_list"][0]["id"] == task.id
+
+
+async def test_task_cache_fails_for_non_unique_names(
+    invoke,
+    register_user,
+    task_factory,
+    workflow_factory,
+    tmp_path: Path,
+    caplog: pytest.LogCaptureFixture,
+    clear_task_cache,
+):
+    """
+    GIVEN two tasks with the same name
+    WHEN the client is invoked to list the tasks
+    THEN
+        * A warning is raised that the cache won't be written
+        * Addressing tasks by name raises a FileNotFoundError
+    """
+
+    # Create two tasks with the same name
+    task1 = await task_factory()
+    task2 = await task_factory()
+    assert task1.name == task2.name
+
+    # Verify that a warning is raised upon creating the cache file
+    caplog.set_level(logging.WARNING)
+    res = await invoke("task list")
+    assert res.retcode == 0
+    debug(caplog.text)
+    assert "Cannot write task-list cache" in caplog.text
+
+    # Verify that adding tasks to a worfklow by name (as opposed to "by id")
+    # fails because of missing cache file
+    wf = await workflow_factory()
+    cmd = f"workflow add-task {wf.id} {task1.name}"
+    debug(cmd)
+    with pytest.raises(FileNotFoundError):
+        res = await invoke(cmd)
+
+
 async def test_edit_workflow_task(
     invoke,
     register_user,
     task_factory,
     workflow_factory,
-    tmp_path,
+    tmp_path: Path,
 ):
     """
     GIVEN a workflow