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

Issue:
Align with fractal-server new feature (project creation does not create a dataset)
Ref https://github.com/fractal-analytics-platform/fractal-server/issues/674

This also includes the `--batch` branch of the project-creation command.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e76a416de..aed224f1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 # Unreleased
 
+* Remove `project new --dataset` argument (\#566).
+* Add `project new --make-read-only` argument (\#566).
+* Do not use deprecated fractal-server `deployment_type` variable (\#566).
 * Automate procedure for publishing package to PyPI (\#561).
 
 # 1.3.6
diff --git a/fractal_client/cmd/__init__.py b/fractal_client/cmd/__init__.py
index cdeaa4ae0..66eecaa4f 100644
--- a/fractal_client/cmd/__init__.py
+++ b/fractal_client/cmd/__init__.py
@@ -63,7 +63,7 @@ async def project(
 ) -> BaseInterface:
 
     if subcmd == "new":
-        parameters = ["name", "dataset"]
+        parameters = ["name"]
         function_kwargs = get_kwargs(parameters, kwargs)
         iface = await post_project(client, batch=batch, **function_kwargs)
     elif subcmd == "show":
@@ -82,7 +82,13 @@ async def project(
         function_kwargs = get_kwargs(parameters, kwargs)
         iface = await patch_project(client, **function_kwargs)
     elif subcmd == "add-dataset":
-        parameters = ["project_id", "dataset_name", "metadata", "type"]
+        parameters = [
+            "project_id",
+            "dataset_name",
+            "metadata",
+            "type",
+            "make_read_only",
+        ]
         function_kwargs = get_kwargs(parameters, kwargs)
         iface = await post_dataset(client, batch=batch, **function_kwargs)
     elif subcmd == "delete":
@@ -319,7 +325,6 @@ async def version(client: AsyncClient, **kwargs) -> PrintInterface:
             f"Fractal client\n\tversion: {__VERSION__}\n"
             "Fractal server:\n"
             f"\turl: {settings.FRACTAL_SERVER}"
-            f"\tdeployment type: {data['deployment_type']}"
             f"\tversion: {data['version']}"
         ),
     )
diff --git a/fractal_client/cmd/_dataset.py b/fractal_client/cmd/_dataset.py
index 25fd8c657..73eec6417 100644
--- a/fractal_client/cmd/_dataset.py
+++ b/fractal_client/cmd/_dataset.py
@@ -20,6 +20,7 @@ async def post_dataset(
     metadata: Optional[str] = None,
     type: Optional[str] = None,
     batch: bool = False,
+    make_read_only: bool = False,
 ) -> RichJsonInterface:
     """
     Arguments:
@@ -27,6 +28,7 @@ async def post_dataset(
         dataset_name: Name of new dataset
         metadata: Path to file containing dataset metadata in JSON format.
         type: Dataset type.
+        make_read_only: Make the new dataset read-only.
     """
     if metadata is None:
         meta = {}
@@ -34,7 +36,7 @@ async def post_dataset(
         with open(metadata, "r") as f:
             meta = json.load(f)
 
-    dataset = dict(name=dataset_name, meta=meta)
+    dataset = dict(name=dataset_name, meta=meta, read_only=make_read_only)
     if type:
         dataset["type"] = type
 
diff --git a/fractal_client/cmd/_project.py b/fractal_client/cmd/_project.py
index a1c5b1e80..9aad70d6c 100644
--- a/fractal_client/cmd/_project.py
+++ b/fractal_client/cmd/_project.py
@@ -16,26 +16,15 @@ async def post_project(
     client: AuthClient,
     *,
     name: str,
-    dataset: Optional[str] = None,
     batch: bool = False,
 ) -> BaseInterface:
     # Prepare a ProjectCreate request body
     project = dict(name=name)
-    if dataset:
-        project["default_dataset_name"] = dataset
-
     # Send API request
     res = await client.post(f"{settings.BASE_URL}/project/", json=project)
     project = check_response(res, expected_status_code=201)
     if batch:
-        if len(project["dataset_list"]) > 1:
-            msg = (
-                f"Created project with {len(project['dataset_list'])}>1 "
-                "datasets, cannot use --batch to provide standard output."
-            )
-            raise ValueError(msg)
-        dataset_id = project["dataset_list"][0]["id"]
-        return PrintInterface(retcode=0, data=f"{project['id']} {dataset_id}")
+        return PrintInterface(retcode=0, data=project["id"])
     else:
         return RichJsonInterface(retcode=0, data=project)
 
diff --git a/fractal_client/parser.py b/fractal_client/parser.py
index 734fa3494..dab92873c 100644
--- a/fractal_client/parser.py
+++ b/fractal_client/parser.py
@@ -66,14 +66,6 @@
     allow_abbrev=False,
 )
 project_new_parser.add_argument("name", help="Name of new project.")
-project_new_parser.add_argument(
-    "-d",
-    "--dataset",
-    help=(
-        "Name of new dataset to create. "
-        "By default, the dataset `default` is created."
-    ),
-)
 
 # project list
 project_list_parser = project_subparsers.add_parser(
@@ -122,6 +114,13 @@
     "--type",
     help="Dataset type.",
 )
+project_add_dataset_parser.add_argument(
+    "--make-read-only",
+    action="store_true",
+    default=False,
+    required=False,
+    help="Make dataset read-only (not required).",
+)
 
 
 # project edit
diff --git a/poetry.lock b/poetry.lock
index ccde8ee34..af876971c 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
 
 [[package]]
 name = "aiosqlite"
@@ -546,50 +546,49 @@ files = [
 
 [[package]]
 name = "fastapi"
-version = "0.95.2"
+version = "0.103.2"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "fastapi-0.95.2-py3-none-any.whl", hash = "sha256:d374dbc4ef2ad9b803899bd3360d34c534adc574546e25314ab72c0c4411749f"},
-    {file = "fastapi-0.95.2.tar.gz", hash = "sha256:4d9d3e8c71c73f11874bcf5e33626258d143252e329a01002f767306c64fb982"},
+    {file = "fastapi-0.103.2-py3-none-any.whl", hash = "sha256:3270de872f0fe9ec809d4bd3d4d890c6d5cc7b9611d721d6438f9dacc8c4ef2e"},
+    {file = "fastapi-0.103.2.tar.gz", hash = "sha256:75a11f6bfb8fc4d2bec0bd710c2d5f2829659c0e8c0afd5560fdda6ce25ec653"},
 ]
 
 [package.dependencies]
-pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0"
+anyio = ">=3.7.1,<4.0.0"
+pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
 starlette = ">=0.27.0,<0.28.0"
+typing-extensions = ">=4.5.0"
 
 [package.extras]
-all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
-dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"]
-doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"]
-test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
+all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
 
 [[package]]
 name = "fastapi-users"
-version = "10.4.2"
+version = "12.1.2"
 description = "Ready-to-use and customizable users management for FastAPI"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "fastapi_users-10.4.2-py3-none-any.whl", hash = "sha256:ce33b3f3f5d765c132e79b20755d05d85e7c2e10dbd23b46ffeb2bb74de2ca82"},
-    {file = "fastapi_users-10.4.2.tar.gz", hash = "sha256:875d43e9c65902f652b939c9d4c95dbb586f01417cfee355ad47989d4dc44cf3"},
+    {file = "fastapi_users-12.1.2-py3-none-any.whl", hash = "sha256:768e3b17b9b89b5f5d3c303afefd69874da0fc97ebac16a86167d4c8d414d479"},
+    {file = "fastapi_users-12.1.2.tar.gz", hash = "sha256:9a9b44e0917c483aaba2bf80ea488b4ba6475b0fb24e2abfaf21e0b5c5db59bf"},
 ]
 
 [package.dependencies]
-email-validator = ">=1.1.0,<1.4"
+email-validator = ">=1.1.0,<2.1"
 fastapi = ">=0.65.2"
-httpx-oauth = {version = ">=0.4,<0.12", optional = true, markers = "extra == \"oauth\""}
+httpx-oauth = {version = ">=0.13", optional = true, markers = "extra == \"oauth\""}
 makefun = ">=1.11.2,<2.0.0"
 passlib = {version = "1.7.4", extras = ["bcrypt"]}
-pyjwt = {version = "2.6.0", extras = ["crypto"]}
+pyjwt = {version = "2.8.0", extras = ["crypto"]}
 python-multipart = "0.0.6"
 
 [package.extras]
-beanie = ["fastapi-users-db-beanie (>=1.0.0)"]
-oauth = ["httpx-oauth (>=0.4,<0.12)"]
-redis = ["redis (>=4.3.3,<5.0.0)"]
-sqlalchemy = ["fastapi-users-db-sqlalchemy (>=4.0.0)"]
+beanie = ["fastapi-users-db-beanie (>=3.0.0)"]
+oauth = ["httpx-oauth (>=0.13)"]
+redis = ["redis (>=4.3.3,<6.0.0)"]
+sqlalchemy = ["fastapi-users-db-sqlalchemy (>=6.0.0)"]
 
 [[package]]
 name = "filelock"
@@ -608,7 +607,7 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "p
 
 [[package]]
 name = "fractal-server"
-version = "1.3.8a0"
+version = "1.3.11"
 description = "Server component of the Fractal analytics platform"
 optional = false
 python-versions = "^3.9"
@@ -618,8 +617,8 @@ develop = false
 [package.dependencies]
 aiosqlite = "^0.17.0"
 alembic = "^1.9.1"
-fastapi = "^0.95.0"
-fastapi-users = {version = "^10.1", extras = ["oauth"]}
+fastapi = "^0.103.0"
+fastapi-users = {version = "^12.1.0", extras = ["oauth"]}
 greenlet = "*"
 pydantic = ">=1.10.8,<2"
 python-dotenv = "^0.20.0"
@@ -637,7 +636,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 = "7ca0df2c3137eda3809a5db19cb228c10f23e22b"
+resolved_reference = "8dde044079096af59e2acb6aa1f0fc52ec66f832"
 
 [[package]]
 name = "ghp-import"
@@ -804,17 +803,17 @@ socks = ["socksio (==1.*)"]
 
 [[package]]
 name = "httpx-oauth"
-version = "0.11.2"
+version = "0.13.0"
 description = "Async OAuth client using HTTPX"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "httpx_oauth-0.11.2-py3-none-any.whl", hash = "sha256:11a2c79111e61a7bd70624606ad6a3013b6756ce4817dd5d5b44f70bacb45940"},
-    {file = "httpx_oauth-0.11.2.tar.gz", hash = "sha256:690e4d0a03b1974b9e600436a6d7a8c064e0707a3d86a41a12153cc797244ac6"},
+    {file = "httpx_oauth-0.13.0-py3-none-any.whl", hash = "sha256:245f04bc8340da4184d26ec0d589e35e7e8f5af6157f30b6e012c0896f80f875"},
+    {file = "httpx_oauth-0.13.0.tar.gz", hash = "sha256:ed7359053ac1b1652adbb18b074b6709bcee54e28fb76dd8062c408cfc4589c3"},
 ]
 
 [package.dependencies]
-httpx = ">=0.18,<0.24"
+httpx = ">=0.18,<0.25"
 
 [[package]]
 name = "identify"
@@ -1397,13 +1396,13 @@ plugins = ["importlib-metadata"]
 
 [[package]]
 name = "pyjwt"
-version = "2.6.0"
+version = "2.8.0"
 description = "JSON Web Token implementation in Python"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "PyJWT-2.6.0-py3-none-any.whl", hash = "sha256:d83c3d892a77bbb74d3e1a2cfa90afaadb60945205d1095d9221f04466f64c14"},
-    {file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
+    {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"},
+    {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"},
 ]
 
 [package.dependencies]
@@ -1831,7 +1830,7 @@ files = [
 ]
 
 [package.dependencies]
-greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"}
+greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\")"}
 
 [package.extras]
 aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]


Test patch:
diff --git a/tests/fixtures_testserver.py b/tests/fixtures_testserver.py
index 5367d4917..b39b01e0a 100644
--- a/tests/fixtures_testserver.py
+++ b/tests/fixtures_testserver.py
@@ -25,7 +25,6 @@ def override_server_settings(tmp_path):
     settings.FRACTAL_RUNNER_BACKEND = "local"
 
     settings.JWT_SECRET_KEY = "secret_key"
-    settings.DEPLOYMENT_TYPE = "development"
     base_folder = tmp_path
     settings.FRACTAL_TASKS_DIR = base_folder / "FRACTAL_TASKS_DIR"
     settings.FRACTAL_RUNNER_WORKING_BASE_DIR = (
diff --git a/tests/test_dataset.py b/tests/test_dataset.py
index 3f453ead2..92d438636 100644
--- a/tests/test_dataset.py
+++ b/tests/test_dataset.py
@@ -41,8 +41,10 @@ async def test_add_resource(register_user, invoke):
     # Create a project with its default dataset
     res = await invoke("project new prj0")
     project_id = res.data["id"]
-    dataset_id = res.data["dataset_list"][0]["id"]
-    assert res.data["dataset_list"][0]["resource_list"] == []
+
+    res = await invoke(f"project add-dataset {project_id} test_name")
+    dataset_id = res.data["id"]
+    assert res.data["resource_list"] == []
 
     # Add a resource
     PATH = "/some/path"
@@ -58,8 +60,10 @@ async def test_add_resource(register_user, invoke):
 async def test_add_resource_relative_path(register_user, invoke):
     res = await invoke("project new prj0")
     project_id = res.data["id"]
-    dataset_id = res.data["dataset_list"][0]["id"]
-    assert res.data["dataset_list"][0]["resource_list"] == []
+
+    res = await invoke(f"project add-dataset {project_id} test_name")
+    dataset_id = res.data["id"]
+    assert res.data["resource_list"] == []
 
     PATH = "../new/resource/path"
     with pytest.raises(SystemExit):
@@ -77,7 +81,9 @@ async def test_add_resource_relative_path(register_user, invoke):
 async def test_edit_dataset(register_user, invoke, tmp_path):
     res = await invoke("project new prj0")
     project_id = res.data["id"]
-    dataset_id = res.data["dataset_list"][0]["id"]
+
+    res = await invoke(f"project add-dataset {project_id} test_name")
+    dataset_id = res.data["id"]
 
     TYPE = "this_new_type"
     NAME = "this_new_name"
@@ -140,7 +146,9 @@ async def test_delete_dataset(register_user, invoke):
     # Create a project with its default dataset
     res = await invoke("project new prj0")
     project_id = res.data["id"]
-    dataset_id = res.data["dataset_list"][0]["id"]
+
+    res = await invoke(f"project add-dataset {project_id} test_name")
+    dataset_id = res.data["id"]
 
     # Delete dataset
     res = await invoke(f"dataset delete {project_id} {dataset_id}")
@@ -154,7 +162,8 @@ async def test_show_dataset(register_user, invoke):
     # Create a project with its default dataset
     res = await invoke("project new prj0")
     project_id = res.data["id"]
-    dataset_id = res.data["dataset_list"][0]["id"]
+    res = await invoke(f"project add-dataset {project_id} test_name")
+    dataset_id = res.data["id"]
 
     res = await invoke(f"dataset show {project_id} {dataset_id}")
     res.show()
@@ -165,8 +174,9 @@ async def test_delete_resource(register_user, invoke):
     # Create a project with its default dataset
     res = await invoke("project new prj0")
     project_id = res.data["id"]
-    dataset_id = res.data["dataset_list"][0]["id"]
-    assert res.data["dataset_list"][0]["resource_list"] == []
+    res = await invoke(f"project add-dataset {project_id} test_name")
+    dataset_id = res.data["id"]
+    assert res.data["resource_list"] == []
 
     # Add a resource
     PATH = "/some/path"
@@ -208,7 +218,8 @@ async def test_dataset_history_command(register_user, invoke):
     """
     res = await invoke("project new prj0")
     project_id = res.data["id"]
-    dataset_id = res.data["dataset_list"][0]["id"]
+    res = await invoke(f"project add-dataset {project_id} test_name")
+    dataset_id = res.data["id"]
 
     debug(f"dataset history {project_id} {dataset_id}")
     res = await invoke(f"dataset history {project_id} {dataset_id}")
@@ -222,7 +233,8 @@ async def test_dataset_status_command(register_user, invoke):
     """
     res = await invoke("project new prj0")
     project_id = res.data["id"]
-    dataset_id = res.data["dataset_list"][0]["id"]
+    res = await invoke(f"project add-dataset {project_id} test_name")
+    dataset_id = res.data["id"]
 
     res = await invoke(f"dataset status {project_id} {dataset_id}")
     res.show()
diff --git a/tests/test_project.py b/tests/test_project.py
index 90d86b1a6..b00adac91 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -4,7 +4,7 @@
 
 async def test_project_create(register_user, invoke):
     PROJECT_NAME = "project_name"
-    res = await invoke(f"project new {PROJECT_NAME} -d foo")
+    res = await invoke(f"project new {PROJECT_NAME}")
     debug(res)
     assert res.data["name"] == PROJECT_NAME
 
@@ -33,9 +33,8 @@ async def test_project_create_batch(register_user, invoke):
     res = await invoke("--batch project new MyProj1")
     debug(res)
     debug(res.data)
-    project_id, dataset_id = map(int, res.data.split())
+    project_id = int(res.data)
     assert project_id == 1
-    assert dataset_id == 1
 
 
 async def test_project_list(register_user, invoke):
@@ -62,12 +61,21 @@ async def test_add_dataset(register_user, invoke):
     res = await invoke("--batch project new proj0")
     assert res.retcode == 0
     debug(res.data)
-    project_id, dataset_id = map(int, res.data.split())
+    project_id = int(res.data)
 
     res = await invoke(f"project add-dataset {project_id} {DATASET_NAME}")
     assert res.retcode == 0
     res.show()
     assert res.data["name"] == DATASET_NAME
+    assert not res.data["read_only"]
+
+    res = await invoke(
+        f"project add-dataset {project_id} new_{DATASET_NAME} --make-read-only"
+    )
+    assert res.retcode == 0
+    res.show()
+    assert res.data["name"] == f"new_{DATASET_NAME}"
+    assert res.data["read_only"]
 
 
 @pytest.mark.parametrize("new_name", ["new_name", None])
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index 86837deac..156b72aeb 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -437,7 +437,8 @@ async def test_workflow_apply(
     assert res.retcode == 0
     prj = res.data
     prj_id = prj["id"]
-    input_dataset_id = prj["dataset_list"][0]["id"]
+    res = await invoke(f"project add-dataset {prj_id} test_name")
+    input_dataset_id = res.data["id"]
 
     # Create output dataset
     res = await invoke(f"project add-dataset {prj_id} {DATASET_NAME}")