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

Issue:
New `fractal workflow {import,export}` commands
As of https://github.com/fractal-analytics-platform/fractal-server/pull/490, the server has new endpoints to import/export a workflow.

The new client commands should look like:
* `fractal workflow import --json-file /some/file.json`, which would first cast the content of `/some/file.json` to a `WorkflowImport` object and then call the appropriate endpoint.
* `fractal workflow export workflow_id --json-file /some/file.json`, which would call the endpoint to export the workflow with ID `workflow_id` and store the output in `/some/file.json`.

Notice that [we also need to update fractal-common](https://github.com/fractal-analytics-platform/fractal-server/pull/490#issuecomment-1420806486), and obviously to install the right version of fractal-server (will be released soon enough).
New `fractal workflow {import,export}` commands
As of https://github.com/fractal-analytics-platform/fractal-server/pull/490, the server has new endpoints to import/export a workflow.

The new client commands should look like:
* `fractal workflow import --json-file /some/file.json`, which would first cast the content of `/some/file.json` to a `WorkflowImport` object and then call the appropriate endpoint.
* `fractal workflow export workflow_id --json-file /some/file.json`, which would call the endpoint to export the workflow with ID `workflow_id` and store the output in `/some/file.json`.

Notice that [we also need to update fractal-common](https://github.com/fractal-analytics-platform/fractal-server/pull/490#issuecomment-1420806486), and obviously to install the right version of fractal-server (will be released soon enough).


Discussion / hints:
The second command should be `fractal workflow export workflow_id`, right?
> The second command should be `fractal workflow export workflow_id`, right?

Sure, thanks 
The second command should be `fractal workflow export workflow_id`, right?
> The second command should be `fractal workflow export workflow_id`, right?

Sure, thanks 

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8f2a2d6fc..dd6a9f214 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 1.0.4
+
+* Add `fractal workflow {import,export}` commands (\#426).
+
 # 1.0.3
 
 * Add `fractal task new` command (\#421).
diff --git a/fractal/cmd/__init__.py b/fractal/cmd/__init__.py
index 3d5ca31b6..7329be5f9 100644
--- a/fractal/cmd/__init__.py
+++ b/fractal/cmd/__init__.py
@@ -35,6 +35,8 @@
 from ._workflow import workflow_delete
 from ._workflow import workflow_edit
 from ._workflow import workflow_edit_task
+from ._workflow import workflow_export
+from ._workflow import workflow_import
 from ._workflow import workflow_list
 from ._workflow import workflow_new
 from ._workflow import workflow_remove_task
@@ -154,6 +156,10 @@ async def workflow(
         iface = await workflow_remove_task(client, **kwargs)
     elif subcmd == "apply":
         iface = await workflow_apply(client, **kwargs)
+    elif subcmd == "import":
+        iface = await workflow_import(client, batch=batch, **kwargs)
+    elif subcmd == "export":
+        iface = await workflow_export(client, **kwargs)
     else:
         raise NoCommandError(f"Command workflow {subcmd} not found")
     return iface
diff --git a/fractal/cmd/_workflow.py b/fractal/cmd/_workflow.py
index dc0062bf7..42f862509 100644
--- a/fractal/cmd/_workflow.py
+++ b/fractal/cmd/_workflow.py
@@ -7,6 +7,8 @@
 from ..common.schemas import ApplyWorkflowCreate
 from ..common.schemas import ApplyWorkflowRead
 from ..common.schemas import WorkflowCreate
+from ..common.schemas import WorkflowExport
+from ..common.schemas import WorkflowImport
 from ..common.schemas import WorkflowRead
 from ..common.schemas import WorkflowTaskCreate
 from ..common.schemas import WorkflowTaskRead
@@ -230,3 +232,50 @@ async def workflow_apply(
         res, expected_status_code=202, coerce=ApplyWorkflowRead
     )
     return RichJsonInterface(retcode=0, data=apply_wf_read.sanitised_dict())
+
+
+async def workflow_import(
+    client: AuthClient,
+    *,
+    project_id: int,
+    json_file: str,
+    batch: bool = False,
+    **kwargs,
+) -> BaseInterface:
+    with Path(json_file).open("r") as f:
+        workflow = json.load(f)
+    workflow = WorkflowImport(**workflow)
+    res = await client.post(
+        f"{settings.BASE_URL}/project/{project_id}/import-workflow/",
+        json=workflow.dict(),
+    )
+    wf_read = check_response(
+        res, expected_status_code=201, coerce=WorkflowRead
+    )
+    if batch:
+        datastr = f"{wf_read.id}"
+        for wftask in wf_read.task_list:
+            datastr += f" {wftask.id}"
+        return PrintInterface(retcode=0, data=datastr)
+    else:
+        return RichJsonInterface(retcode=0, data=wf_read.dict())
+
+
+async def workflow_export(
+    client: AuthClient,
+    *,
+    workflow_id: int,
+    json_file: str,
+    **kwargs,
+) -> BaseInterface:
+    res = await client.get(
+        f"{settings.BASE_URL}/workflow/{workflow_id}/export/"
+    )
+    workflow = check_response(
+        res, expected_status_code=200, coerce=WorkflowExport
+    )
+    with Path(json_file).open("w") as f:
+        json.dump(workflow.dict(), f)
+    return PrintInterface(
+        retcode=0, data=f"Workflow {workflow_id} exported at {json_file}"
+    )
diff --git a/fractal/common b/fractal/common
index 0b46848de..49d21a735 160000
--- a/fractal/common
+++ b/fractal/common
@@ -1,1 +1,1 @@
-Subproject commit 0b46848ded8b1a70e67a60b49a5588654a5ef899
+Subproject commit 49d21a735876e56aa08d8e5bb682f73196c59427
diff --git a/fractal/parser.py b/fractal/parser.py
index 3f5616b8e..e10871d05 100644
--- a/fractal/parser.py
+++ b/fractal/parser.py
@@ -366,11 +366,6 @@
     help="Workflow ID",
 )
 workflow_edit_parser.add_argument("--name", type=str, help="New workflow name")
-workflow_edit_parser.add_argument(
-    "--project-id",
-    type=int,
-    help="ID of the project to be associated with the current workflow",
-)
 
 # workflow delete
 workflow_delete_parser = workflow_subparsers.add_parser(
@@ -479,6 +474,36 @@
     help="Command to be run before starting a worker",
 )
 
+# workflow import
+workflow_import_parser = workflow_subparsers.add_parser(
+    "import",
+    description="Import workflow to project from file",
+)
+workflow_import_parser.add_argument(
+    "--project-id",
+    help="ID of the project where the workflow will be imported",
+    required=True,
+)
+workflow_import_parser.add_argument(
+    "--json-file",
+    help="Path to a JSON file with the workflow to be imported",
+)
+
+# workflow export
+workflow_export_parser = workflow_subparsers.add_parser(
+    "export",
+    description="Export workflow to file",
+)
+workflow_export_parser.add_argument(
+    "--workflow-id",
+    help="Workflow ID",
+    required=True,
+)
+workflow_export_parser.add_argument(
+    "--json-file",
+    help="Path to the JSON file where the workflow will be exported",
+    required=True,
+)
 
 # JOB GROUP
 
diff --git a/poetry.lock b/poetry.lock
index c9f3a5639..c0ecc085e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -29,14 +29,14 @@ files = [
 
 [[package]]
 name = "alembic"
-version = "1.9.2"
+version = "1.9.3"
 description = "A database migration tool for SQLAlchemy."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "alembic-1.9.2-py3-none-any.whl", hash = "sha256:e8a6ff9f3b1887e1fed68bfb8fb9a000d8f61c21bdcc85b67bb9f87fcbc4fce3"},
-    {file = "alembic-1.9.2.tar.gz", hash = "sha256:6880dec4f28dd7bd999d2ed13fbe7c9d4337700a44d11a524c0ce0c59aaf0dbd"},
+    {file = "alembic-1.9.3-py3-none-any.whl", hash = "sha256:ed2f73ea9c986f43af8ad7502c5f60d6bb1400bcd6d29f230e760e08884cb476"},
+    {file = "alembic-1.9.3.tar.gz", hash = "sha256:8fd6aaea56f5a703a190d25a705dfa91d7c313bb71de2f9c68f5abdcaf5df164"},
 ]
 
 [package.dependencies]
@@ -473,47 +473,49 @@ toml = ["tomli"]
 
 [[package]]
 name = "cryptography"
-version = "39.0.0"
+version = "39.0.1"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
 category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"},
-    {file = "cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"},
-    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"},
-    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"},
-    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"},
-    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"},
-    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"},
-    {file = "cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"},
-    {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"},
-    {file = "cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"},
-    {file = "cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"},
-    {file = "cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"},
-    {file = "cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"},
-    {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"},
-    {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"},
-    {file = "cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"},
-    {file = "cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"},
-    {file = "cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"},
-    {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"},
-    {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"},
-    {file = "cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"},
-    {file = "cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"},
-    {file = "cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"},
+    {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:6687ef6d0a6497e2b58e7c5b852b53f62142cfa7cd1555795758934da363a965"},
+    {file = "cryptography-39.0.1-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:706843b48f9a3f9b9911979761c91541e3d90db1ca905fd63fee540a217698bc"},
+    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5d2d8b87a490bfcd407ed9d49093793d0f75198a35e6eb1a923ce1ee86c62b41"},
+    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e17b26de248c33f3acffb922748151d71827d6021d98c70e6c1a25ddd78505"},
+    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e124352fd3db36a9d4a21c1aa27fd5d051e621845cb87fb851c08f4f75ce8be6"},
+    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:5aa67414fcdfa22cf052e640cb5ddc461924a045cacf325cd164e65312d99502"},
+    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:35f7c7d015d474f4011e859e93e789c87d21f6f4880ebdc29896a60403328f1f"},
+    {file = "cryptography-39.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f24077a3b5298a5a06a8e0536e3ea9ec60e4c7ac486755e5fb6e6ea9b3500106"},
+    {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f0c64d1bd842ca2633e74a1a28033d139368ad959872533b1bab8c80e8240a0c"},
+    {file = "cryptography-39.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4"},
+    {file = "cryptography-39.0.1-cp36-abi3-win32.whl", hash = "sha256:fe913f20024eb2cb2f323e42a64bdf2911bb9738a15dba7d3cce48151034e3a8"},
+    {file = "cryptography-39.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:ced4e447ae29ca194449a3f1ce132ded8fcab06971ef5f618605aacaa612beac"},
+    {file = "cryptography-39.0.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:807ce09d4434881ca3a7594733669bd834f5b2c6d5c7e36f8c00f691887042ad"},
+    {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5caeb8188c24888c90b5108a441c106f7faa4c4c075a2bcae438c6e8ca73cef"},
+    {file = "cryptography-39.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4789d1e3e257965e960232345002262ede4d094d1a19f4d3b52e48d4d8f3b885"},
+    {file = "cryptography-39.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:96f1157a7c08b5b189b16b47bc9db2332269d6680a196341bf30046330d15388"},
+    {file = "cryptography-39.0.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e422abdec8b5fa8462aa016786680720d78bdce7a30c652b7fadf83a4ba35336"},
+    {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:b0afd054cd42f3d213bf82c629efb1ee5f22eba35bf0eec88ea9ea7304f511a2"},
+    {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:6f8ba7f0328b79f08bdacc3e4e66fb4d7aab0c3584e0bd41328dce5262e26b2e"},
+    {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ef8b72fa70b348724ff1218267e7f7375b8de4e8194d1636ee60510aae104cd0"},
+    {file = "cryptography-39.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:aec5a6c9864be7df2240c382740fcf3b96928c46604eaa7f3091f58b878c0bb6"},
+    {file = "cryptography-39.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdd188c8a6ef8769f148f88f859884507b954cc64db6b52f66ef199bb9ad660a"},
+    {file = "cryptography-39.0.1.tar.gz", hash = "sha256:d1f6198ee6d9148405e49887803907fe8962a23e6c6f83ea7d98f1c0de375695"},
 ]
 
 [package.dependencies]
 cffi = ">=1.12"
 
 [package.extras]
-docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1,!=5.2.0,!=5.2.0.post0)", "sphinx-rtd-theme"]
+docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
 docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
-pep8test = ["black", "ruff"]
+pep8test = ["black", "check-manifest", "mypy", "ruff", "types-pytz", "types-requests"]
 sdist = ["setuptools-rust (>=0.11.4)"]
 ssh = ["bcrypt (>=3.1.5)"]
-test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"]
+test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist", "pytz"]
+test-randomorder = ["pytest-randomly"]
+tox = ["tox"]
 
 [[package]]
 name = "devtools"
@@ -557,14 +559,14 @@ wmi = ["wmi (>=1.5.1,<2.0.0)"]
 
 [[package]]
 name = "docutils"
-version = "0.17.1"
+version = "0.18.1"
 description = "Docutils -- Python Documentation Utilities"
 category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 files = [
-    {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"},
-    {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"},
+    {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"},
+    {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"},
 ]
 
 [[package]]
@@ -678,14 +680,14 @@ sqlmodel = "*"
 
 [[package]]
 name = "fractal-server"
-version = "1.0.4"
+version = "1.0.7a1"
 description = "Server component of the Fractal analytics platform"
 category = "dev"
 optional = false
 python-versions = ">=3.8,<4.0"
 files = [
-    {file = "fractal_server-1.0.4-py3-none-any.whl", hash = "sha256:395ad605486a6d0b0784ad38607207b65c09dee5785906bb72a58fbbdbf0c485"},
-    {file = "fractal_server-1.0.4.tar.gz", hash = "sha256:c560eaa3de5d9e124a7d281ba5854662e3bf6a4a411dc862223a83dc6de08973"},
+    {file = "fractal_server-1.0.7a1-py3-none-any.whl", hash = "sha256:5fc359f8e960b02c1e76fec4f41b793d75dff96f9d545f742bfcdb26faa7bcf0"},
+    {file = "fractal_server-1.0.7a1.tar.gz", hash = "sha256:5c2d9a9553873d3c5210787ccc643ae9614fee51b6cdaabad46bdd628efd48b4"},
 ]
 
 [package.dependencies]
@@ -1509,14 +1511,14 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
 
 [[package]]
 name = "setuptools"
-version = "67.1.0"
+version = "67.2.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-67.1.0-py3-none-any.whl", hash = "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378"},
-    {file = "setuptools-67.1.0.tar.gz", hash = "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300"},
+    {file = "setuptools-67.2.0-py3-none-any.whl", hash = "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c"},
+    {file = "setuptools-67.2.0.tar.gz", hash = "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48"},
 ]
 
 [package.extras]
@@ -1616,19 +1618,20 @@ markdown = ["CommonMark (>=0.5.6)"]
 
 [[package]]
 name = "sphinx-rtd-theme"
-version = "1.1.1"
+version = "1.2.0"
 description = "Read the Docs theme for Sphinx"
 category = "dev"
 optional = false
-python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
 files = [
-    {file = "sphinx_rtd_theme-1.1.1-py2.py3-none-any.whl", hash = "sha256:31faa07d3e97c8955637fc3f1423a5ab2c44b74b8cc558a51498c202ce5cbda7"},
-    {file = "sphinx_rtd_theme-1.1.1.tar.gz", hash = "sha256:6146c845f1e1947b3c3dd4432c28998a1693ccc742b4f9ad7c63129f0757c103"},
+    {file = "sphinx_rtd_theme-1.2.0-py2.py3-none-any.whl", hash = "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2"},
+    {file = "sphinx_rtd_theme-1.2.0.tar.gz", hash = "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8"},
 ]
 
 [package.dependencies]
-docutils = "<0.18"
-sphinx = ">=1.6,<6"
+docutils = "<0.19"
+sphinx = ">=1.6,<7"
+sphinxcontrib-jquery = {version = ">=2.0.0,<3.0.0 || >3.0.0", markers = "python_version > \"3\""}
 
 [package.extras]
 dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"]
@@ -1681,6 +1684,21 @@ files = [
 lint = ["docutils-stubs", "flake8", "mypy"]
 test = ["html5lib", "pytest"]
 
+[[package]]
+name = "sphinxcontrib-jquery"
+version = "2.0.0"
+description = "Extension to include jQuery on newer Sphinx releases"
+category = "dev"
+optional = false
+python-versions = ">=2.7"
+files = [
+    {file = "sphinxcontrib-jquery-2.0.0.tar.gz", hash = "sha256:8fb65f6dba84bf7bcd1aea1f02ab3955ac34611d838bcc95d4983b805b234daa"},
+    {file = "sphinxcontrib_jquery-2.0.0-py3-none-any.whl", hash = "sha256:ed47fa425c338ffebe3c37e1cdb56e30eb806116b85f01055b158c7057fdb995"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
 [[package]]
 name = "sphinxcontrib-jsmath"
 version = "1.0.1"
@@ -1952,14 +1970,14 @@ standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)",
 
 [[package]]
 name = "zipp"
-version = "3.12.1"
+version = "3.13.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "zipp-3.12.1-py3-none-any.whl", hash = "sha256:6c4fe274b8f85ec73c37a8e4e3fa00df9fb9335da96fb789e3b96b318e5097b3"},
-    {file = "zipp-3.12.1.tar.gz", hash = "sha256:a3cac813d40993596b39ea9e93a18e8a2076d5c378b8bc88ec32ab264e04ad02"},
+    {file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"},
+    {file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"},
 ]
 
 [package.extras]
@@ -1969,4 +1987,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "5327728a821166604c26fbfe281de4c0ef54ff5bb57c6ee1f983a57de59dc7c7"
+content-hash = "7c197ab6ae3a4af370b2b5ba54b8d0f626d743357b9e7be274960f1725935a73"
diff --git a/pyproject.toml b/pyproject.toml
index 3c484e766..210ad8b6a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,7 @@ devtools = "^0.9.0"
 pytest = "^7.1.2"
 bumpver = "^2022.1118"
 pytest-asyncio = "^0.19.0"
-fractal-server = "^1.0.4"
+fractal-server = "1.0.7a1"
 coverage = {extras = ["toml"], version = "^6.5.0"}
 pytest-pretty = "^1.0.1"
 


Test patch:
diff --git a/tests/data/import-export/workflow.json b/tests/data/import-export/workflow.json
new file mode 100644
index 000000000..65dd1c8b5
--- /dev/null
+++ b/tests/data/import-export/workflow.json
@@ -0,0 +1,45 @@
+{
+    "name": "MyWorkflow",
+    "task_list": [
+        {
+            "args": {
+                "message": "task 0",
+                "arg": "value"
+            },
+            "meta": {"key": "value"},
+            "order": 0,
+            "task": {
+                "command": "/SOME/PATH/dummy0/venv/bin/python /SOME/PATH/dummy0/venv/lib/python3.8/site-packages/fractal_tasks_dummy/dummy.py",
+                "default_args": {
+                    "index": 0,
+                    "message": "default message",
+                    "raise_error": false,
+                    "sleep_time": 0
+                },
+                "input_type": "Any",
+                "meta": {},
+                "name": "dummy",
+                "output_type": "Any",
+                "source": "pip-local:fractal_tasks_dummy-0.1.0-py3-none-any.whl"
+            }
+        },
+        {
+            "args": {
+                "message": "task 1"
+            },
+            "order": 1,
+            "task": {
+                "command": "/SOME/PATH/dummy0/venv/bin/python /SOME/PATH/dummy0/venv/lib/python3.8/site-packages/fractal_tasks_dummy/dummy_parallel.py",
+                "default_args": {
+                    "message": "default message",
+                    "raise_error": false
+                },
+                "input_type": "Any",
+                "meta": {"parallelization_level": "index"},
+                "name": "dummy parallel",
+                "output_type": "Any",
+                "source": "pip-local:fractal_tasks_dummy-0.1.0-py3-none-any.whl"
+            }
+        }
+    ]
+}
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index a0c01e8f9..88fa6c859 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -54,48 +54,29 @@ async def test_workflow_delete(register_user, invoke):
 
 
 async def test_workflow_edit(register_user, invoke):
-    # Create two projects
-    res_pj_1 = await invoke("project new project_name_1 /some/path")
-    res_pj_2 = await invoke("project new project_name_2 /some/path")
-    assert res_pj_1.retcode == 0
-    assert res_pj_2.retcode == 0
-    project_id_1 = res_pj_1.data["id"]
-    project_id_2 = res_pj_2.data["id"]
-
-    # Add a workflow to project 1
-    res_wf = await invoke(f"workflow new MyWorkflow {project_id_1}")
+    # Create a project
+    res_pj = await invoke("project new project_name_1 /some/path")
+    assert res_pj.retcode == 0
+    project_id = res_pj.data["id"]
+
+    # Add a workflow
+    res_wf = await invoke(f"workflow new MyWorkflow {project_id}")
     workflow_id = res_wf.data["id"]
     assert res_wf.retcode == 0
 
-    # List workflows
-    res_list_1 = await invoke(f"workflow list {project_id_1}")
-    res_list_2 = await invoke(f"workflow list {project_id_2}")
-    assert res_list_1.retcode == 0
-    assert res_list_2.retcode == 0
-    assert len(res_list_1.data) == 1
-    assert len(res_list_2.data) == 0
-    assert res_list_1.data[0]["id"] == workflow_id
-
-    # Edit workflow's name and project
-    NAME = "New workflow name"
-    cmd = (
-        f"workflow edit {workflow_id} "
-        f'--name "{NAME}" --project-id {project_id_2}'
-    )
+    # Edit workflow name
+    NAME = "new-workflow-name"
+    cmd = f"workflow edit {workflow_id} --name {NAME}"
     debug(cmd)
     res_edit = await invoke(cmd)
     assert res_edit.retcode == 0
     debug(res_edit.data)
 
     # List workflows, and check edit
-    res_list_1 = await invoke(f"workflow list {project_id_1}")
-    res_list_2 = await invoke(f"workflow list {project_id_2}")
-    assert res_list_1.retcode == 0
-    assert res_list_2.retcode == 0
-    assert len(res_list_1.data) == 0
-    assert len(res_list_2.data) == 1
-    assert res_list_2.data[0]["id"] == workflow_id
-    assert res_list_2.data[0]["name"] == NAME
+    res = await invoke(f"workflow show {workflow_id}")
+    debug(res.data)
+    assert res.retcode == 0
+    assert res.data["name"] == NAME
 
 
 async def test_workflow_list(register_user, invoke):
@@ -484,3 +465,73 @@ async def test_workflow_apply(
     assert res.data["log"] is not None
     # Note: the failing task is not added to the history
     assert len(res.data["history"]) > 0
+
+
+async def test_workflow_export(
+    register_user,
+    invoke,
+    workflow_factory,
+    tmp_path: Path,
+):
+    NAME = "WorkFlow"
+    wf = await workflow_factory(name=NAME)
+    wf_id = wf.id
+    filename = str(tmp_path / "exported_wf.json")
+    res = await invoke(
+        f"workflow export --workflow-id {wf_id} --json-file {filename}"
+    )
+    debug(res.data)
+    assert res.retcode == 0
+    with open(filename, "r") as f:
+        exported_wf = json.load(f)
+        assert exported_wf["name"] == NAME
+        assert "id" not in exported_wf
+        assert "project_id" not in exported_wf
+        for wftask in exported_wf["task_list"]:
+            assert "id" not in wftask
+            assert "task_id" not in wftask
+            assert "workflow_id" not in wftask
+
+
+async def test_workflow_import(
+    register_user,
+    invoke,
+    workflow_factory,
+    tmp_path: Path,
+    testdata_path: Path,
+    project_factory,
+):
+    # collect tasks
+    PACKAGE_NAME = testdata_path / "fractal_tasks_dummy-0.1.0-py3-none-any.whl"
+    res0 = await invoke(f"task collect {PACKAGE_NAME}")
+    assert res0.retcode == 0
+    state_id = res0.data["id"]
+    starting_time = time.perf_counter()
+    while True:
+        res1 = await invoke(f"task check-collection {state_id}")
+        if res1.data["data"]["status"] == "OK":
+            break
+        await asyncio.sleep(1)
+        assert time.perf_counter() - starting_time < TIMEOUT
+
+    # create project
+    PROJECT_NAME = "project_name"
+    PROJECT_PATH = str(tmp_path / "project_path")
+    res_pj = await invoke(f"project new {PROJECT_NAME} {PROJECT_PATH}")
+    assert res_pj.retcode == 0
+    project_id = res_pj.data["id"]
+
+    # import workflow into project
+    filename = str(testdata_path / "import-export/workflow.json")
+    res = await invoke(
+        f"workflow import --project-id {project_id} --json-file {filename}"
+    )
+    debug(res.data)
+    assert res.retcode == 0
+    imported_workflow = res.data
+
+    # get the workflow from the server, and check that it is the same
+    workflow_id = res.data["id"]
+    res = await invoke(f"workflow show {workflow_id}")
+    assert res.retcode == 0
+    assert res.data == imported_workflow