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

Issue:
Investigate `database is locked` CI failures
E.g. https://github.com/fractal-analytics-platform/fractal/actions/runs/5058083170/jobs/9077887082.

Update common and remove sqlmodel dependency
Refs:
* https://github.com/fractal-analytics-platform/fractal-common/issues/59
* https://github.com/fractal-analytics-platform/fractal-common/pull/65

(note that sqlmodel will remain, but only as a dev dependency)
Investigate `database is locked` CI failures
E.g. https://github.com/fractal-analytics-platform/fractal/actions/runs/5058083170/jobs/9077887082.



Discussion / hints:
Also: https://github.com/fractal-analytics-platform/fractal/actions/runs/5067313808/jobs/9098221183

Also: https://github.com/fractal-analytics-platform/fractal/actions/runs/5067313808/jobs/9098221183

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f45dc3e9..d679dc516 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,14 +2,21 @@
 
 # 1.3.0
 
-* Align with [fractal-server 1.2.0](https://fractal-analytics-platform.github.io/fractal-server/changelog/#130), by updating all relevant endpoint path/query/body parameters (\#479).
-* Make `project_id` a required positional argument of `fractal {workflow,dataset,job}` commands (\#479).
-* For `fractal workflow apply`, transform `project_id` from option to positional argument (\#479).
+* Align with [fractal-server 1.3.0](https://fractal-analytics-platform.github.io/fractal-server/changelog/#130), by updating all relevant endpoint path/query/body parameters (\#479).
 * Add `fractal job stop` command (\#485).
-* Add `--type` optional argument to `fractal dataset new` command (\#479).
-* Make `output_dataset_id` a required positional argument of `fractal workflow apply` (\#483).
-* 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).
+* Specific changes to existing commands:
+    * Make `project_id` a required positional argument of `fractal {workflow,dataset,job}` commands (\#479).
+    * Add `--type` optional argument to `fractal dataset new` command (\#479).
+    * For `fractal workflow apply`:
+        * Ttransform `project_id` from option to positional argument (\#479).
+        * Make `output_dataset_id` a required positional argument (\#483).
+    * Add `--username/--new-username` to `fractal user` subcommands (\#493).
+    * Remove `--private` option for `fractal task collect` (\#493).
+    * Add `--version` to `fractal task {new,edit}` subcommands (\#493).
+* 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).
+    * Make `sqlmodel` a development depedency (\#493).
 
 # 1.2.0
 
diff --git a/fractal/cmd/_task.py b/fractal/cmd/_task.py
index 8f9f04063..7900682f2 100644
--- a/fractal/cmd/_task.py
+++ b/fractal/cmd/_task.py
@@ -26,22 +26,25 @@ async def task_collect_pip(
     *,
     package: str,
     package_version: Optional[str] = None,
-    python_version: Optional[str],
+    python_version: Optional[str] = None,
     package_extras: Optional[str] = None,
-    private: Optional[bool] = False,
     batch: bool = False,
     **kwargs,
 ) -> BaseInterface:
-    task_collect = TaskCollectPip(
-        package=package,
-        version=package_version,
-        python_version=python_version,
-        package_extras=package_extras,
-    )
+
+    # Construct TaskCollectPip object
+    attributes = dict(package=package)
+    if package_version:
+        attributes["package_version"] = package_version
+    if python_version:
+        attributes["python_version"] = python_version
+    if package_extras:
+        attributes["package_extras"] = package_extras
+    task_collect = TaskCollectPip(**attributes)
 
     res = await client.post(
-        f"{settings.BASE_URL}/task/collect/pip/?public={not private}",
-        json=task_collect.dict(),
+        f"{settings.BASE_URL}/task/collect/pip/",
+        json=task_collect.dict(exclude_unset=True),
     )
 
     state = check_response(
@@ -92,13 +95,16 @@ async def post_task(
     command: str,
     source: str,
     batch: bool = False,
-    input_type: Optional[str] = "Any",
-    output_type: Optional[str] = "Any",
-    default_args_file: Optional[str] = None,
+    input_type: str = "Any",
+    output_type: str = "Any",
+    version: Optional[str] = None,
     meta_file: Optional[str] = None,
+    default_args_file: Optional[str] = None,
     **kwargs,
 ) -> BaseInterface:
     optionals = {}
+    if version:
+        optionals["version"] = version
     if default_args_file:
         with open(default_args_file, "r") as f:
             optionals["default_args"] = json.load(f)
@@ -130,6 +136,7 @@ async def patch_task(
     command: Optional[str] = None,
     input_type: Optional[str] = None,
     output_type: Optional[str] = None,
+    version: Optional[str] = None,
     default_args_file: Optional[str] = None,
     meta_file: Optional[str] = None,
     **kwargs,
@@ -139,6 +146,8 @@ async def patch_task(
         update["name"] = name
     if command:
         update["command"] = command
+    if version:
+        update["version"] = version
     if input_type:
         update["input_type"] = input_type
     if output_type:
@@ -150,7 +159,7 @@ async def patch_task(
         with open(meta_file, "r") as f:
             update["meta"] = json.load(f)
 
-    task_update = TaskUpdate(**update)  # validation
+    task_update = TaskUpdate(**update)
     payload = task_update.dict(exclude_unset=True)
     if not payload:
         return PrintInterface(retcode=1, data="Nothing to update")
@@ -174,9 +183,3 @@ async def delete_task(
 ) -> PrintInterface:
 
     raise NotImplementedError("task_delete")
-
-    # res = await client.delete(
-    #    f"{settings.BASE_URL}/xxxx"
-    # )
-    # check_response(res, expected_status_code=204)
-    # return PrintInterface(retcode=0, data="")
diff --git a/fractal/cmd/_user.py b/fractal/cmd/_user.py
index f8c875ca8..b0beba5f7 100644
--- a/fractal/cmd/_user.py
+++ b/fractal/cmd/_user.py
@@ -17,6 +17,7 @@ async def user_register(
     new_password: Optional[str] = None,
     slurm_user: Optional[str] = None,
     cache_dir: Optional[str] = None,
+    username: Optional[str] = None,
     superuser: bool = False,
     batch: bool = False,
     **kwargs,
@@ -30,6 +31,8 @@ async def user_register(
         user_dict["slurm_user"] = slurm_user
     if cache_dir:
         user_dict["cache_dir"] = cache_dir
+    if username:
+        user_dict["username"] = username
     new_user = UserCreate(**user_dict)
 
     from getpass import getpass
@@ -90,6 +93,7 @@ async def user_edit(
     new_password: Optional[str] = None,
     new_slurm_user: Optional[str] = None,
     new_cache_dir: Optional[str] = None,
+    new_username: Optional[str] = None,
     make_superuser: bool = False,
     remove_superuser: bool = False,
     **kwargs,
@@ -102,6 +106,8 @@ async def user_edit(
         user_dict["cache_dir"] = new_cache_dir
     if new_slurm_user is not None:
         user_dict["slurm_user"] = new_slurm_user
+    if new_username is not None:
+        user_dict["username"] = new_username
     user_update = UserUpdate(**user_dict)
 
     if make_superuser:
diff --git a/fractal/common b/fractal/common
index a8db2ef55..049a1c5c4 160000
--- a/fractal/common
+++ b/fractal/common
@@ -1,1 +1,1 @@
-Subproject commit a8db2ef55cd63e49d423437ba1e0267ae82dfd5b
+Subproject commit 049a1c5c439ef3e4d3d21d093d0d6f72fedb94d4
diff --git a/fractal/parser.py b/fractal/parser.py
index 8ce9cefb8..d6a30bd69 100644
--- a/fractal/parser.py
+++ b/fractal/parser.py
@@ -295,12 +295,6 @@
         "`fractal-tasks-core[torch,tensorflow]`"
     ),
 )
-task_collect_parser.add_argument(
-    "--private",
-    default=False,
-    action="store_true",
-    help="Intall tasks as private to the user (as opposed to global)",
-)
 
 # task check-collection
 task_check_collection_parser = task_subparsers.add_parser(
@@ -356,6 +350,11 @@
     type=str,
     default="Any",
 )
+task_new_parser.add_argument(
+    "--version",
+    type=str,
+    help="Task version",
+)
 task_new_parser.add_argument(
     "--default-args-file",
     help="Path to JSON file with default task arguments",
@@ -377,18 +376,21 @@
 task_edit_parser.add_argument(
     "task_id_or_name", help="ID or name of task to edit", type=str
 )
-task_edit_parser.add_argument("--name", help="New task name")
-task_edit_parser.add_argument("--command", help="New task command")
+task_edit_parser.add_argument("--name", help="New task name", type=str)
+task_edit_parser.add_argument("--command", help="New task command", type=str)
 task_edit_parser.add_argument(
     "--input-type",
+    type=str,
     help="New input type",
 )
 task_edit_parser.add_argument(
     "--output-type",
+    type=str,
     help="New output type",
 )
 task_edit_parser.add_argument(
     "--default-args-file",
+    type=str,
     help=(
         "Path to JSON serialised file containing "
         "the task default arguments dictionary"
@@ -396,8 +398,15 @@
 )
 task_edit_parser.add_argument(
     "--meta-file",
+    type=str,
     help="Path to JSON serialised file containing the task meta dictionary",
 )
+task_edit_parser.add_argument(
+    "--version",
+    type=str,
+    help="New version",
+)
+
 
 # task delete
 task_delete_parser = task_subparsers.add_parser(
@@ -736,6 +745,11 @@
     help="Username to login into SLURM cluster",
     required=False,
 )
+user_register_parser.add_argument(
+    "--username",
+    help="Username associated to the user",
+    required=False,
+)
 user_register_parser.add_argument(
     "--superuser",
     help="Give superuser privileges to the new user",
@@ -783,7 +797,9 @@
 user_edit_parser.add_argument(
     "--new-slurm-user", help="New SLURM username", type=str, required=False
 )
-
+user_edit_parser.add_argument(
+    "--new-username", help="New user username", type=str, required=False
+)
 user_edit_parser_superuser = user_edit_parser.add_mutually_exclusive_group()
 user_edit_parser_superuser.add_argument(
     "--make-superuser",
diff --git a/poetry.lock b/poetry.lock
index 9d273b812..1eaf12a7f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -29,14 +29,14 @@ files = [
 
 [[package]]
 name = "alembic"
-version = "1.10.4"
+version = "1.11.1"
 description = "A database migration tool for SQLAlchemy."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "alembic-1.10.4-py3-none-any.whl", hash = "sha256:43942c3d4bf2620c466b91c0f4fca136fe51ae972394a0cc8b90810d664e4f5c"},
-    {file = "alembic-1.10.4.tar.gz", hash = "sha256:295b54bbb92c4008ab6a7dcd1e227e668416d6f84b98b3c4446a2bc6214a556b"},
+    {file = "alembic-1.11.1-py3-none-any.whl", hash = "sha256:dc871798a601fab38332e38d6ddb38d5e734f60034baeb8e2db5b642fccd8ab8"},
+    {file = "alembic-1.11.1.tar.gz", hash = "sha256:6a810a6b012c88b33458fceb869aef09ac75d6ace5291915ba7fae44de372c01"},
 ]
 
 [package.dependencies]
@@ -574,19 +574,19 @@ files = [
 
 [[package]]
 name = "fastapi"
-version = "0.95.1"
+version = "0.95.2"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "fastapi-0.95.1-py3-none-any.whl", hash = "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"},
-    {file = "fastapi-0.95.1.tar.gz", hash = "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5"},
+    {file = "fastapi-0.95.2-py3-none-any.whl", hash = "sha256:d374dbc4ef2ad9b803899bd3360d34c534adc574546e25314ab72c0c4411749f"},
+    {file = "fastapi-0.95.2.tar.gz", hash = "sha256:4d9d3e8c71c73f11874bcf5e33626258d143252e329a01002f767306c64fb982"},
 ]
 
 [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"
-starlette = ">=0.26.1,<0.27.0"
+starlette = ">=0.27.0,<0.28.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)"]
@@ -623,7 +623,7 @@ sqlalchemy = ["fastapi-users-db-sqlalchemy (>=4.0.0)"]
 
 [[package]]
 name = "fractal-server"
-version = "1.3.0a1"
+version = "1.3.0a2"
 description = "Server component of the Fractal analytics platform"
 category = "dev"
 optional = false
@@ -636,6 +636,7 @@ aiosqlite = "^0.17.0"
 alembic = "^1.9.1"
 fastapi = "^0.95.0"
 fastapi-users = {version = "^10.1", extras = ["oauth"]}
+pydantic = ">=1.10.8"
 python-dotenv = "^0.20.0"
 sqlalchemy = "^1.4"
 SQLAlchemy-Utils = "^0.38.3"
@@ -651,13 +652,13 @@ 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 = "038d66488e9b65bbdfc81a2cc42fb5c1b94b70f8"
+resolved_reference = "94104124e464516aae53f016bf132920bcf7c808"
 
 [[package]]
 name = "greenlet"
 version = "2.0.2"
 description = "Lightweight in-process concurrent programming"
-category = "main"
+category = "dev"
 optional = false
 python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
 files = [
@@ -1366,14 +1367,14 @@ files = [
 
 [[package]]
 name = "requests"
-version = "2.30.0"
+version = "2.31.0"
 description = "Python HTTP for Humans."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "requests-2.30.0-py3-none-any.whl", hash = "sha256:10e94cc4f3121ee6da529d358cdaeaff2f1c409cd377dbc72b825852f2f7e294"},
-    {file = "requests-2.30.0.tar.gz", hash = "sha256:239d7d4458afcb28a692cdd298d87542235f4ca8d36d03a15bfc128a6559a2f4"},
+    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
+    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
 ]
 
 [package.dependencies]
@@ -1425,19 +1426,19 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
 
 [[package]]
 name = "setuptools"
-version = "67.7.2"
+version = "67.8.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-67.7.2-py3-none-any.whl", hash = "sha256:23aaf86b85ca52ceb801d32703f12d77517b2556af839621c641fca11287952b"},
-    {file = "setuptools-67.7.2.tar.gz", hash = "sha256:f104fa03692a2602fa0fec6c6a9e63b6c8a968de13e17c026957dd1f53d80990"},
+    {file = "setuptools-67.8.0-py3-none-any.whl", hash = "sha256:5df61bf30bb10c6f756eb19e7c9f3b473051f48db77fddbe06ff2ca307df9a6f"},
+    {file = "setuptools-67.8.0.tar.gz", hash = "sha256:62642358adc77ffa87233bc4d2354c4b2682d214048f500964dbe760ccedf102"},
 ]
 
 [package.extras]
 docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
 [[package]]
@@ -1532,14 +1533,14 @@ markdown = ["CommonMark (>=0.5.6)"]
 
 [[package]]
 name = "sphinx-rtd-theme"
-version = "1.2.0"
+version = "1.2.1"
 description = "Read the Docs theme for Sphinx"
 category = "dev"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
 files = [
-    {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"},
+    {file = "sphinx_rtd_theme-1.2.1-py2.py3-none-any.whl", hash = "sha256:2cc9351176cbf91944ce44cefd4fab6c3b76ac53aa9e15d6db45a3229ad7f866"},
+    {file = "sphinx_rtd_theme-1.2.1.tar.gz", hash = "sha256:cf9a7dc0352cf179c538891cb28d6fad6391117d4e21c891776ab41dd6c8ff70"},
 ]
 
 [package.dependencies]
@@ -1664,7 +1665,7 @@ test = ["pytest"]
 name = "sqlalchemy"
 version = "1.4.41"
 description = "Database Abstraction Library"
-category = "main"
+category = "dev"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
 files = [
@@ -1768,7 +1769,7 @@ url = ["furl (>=0.4.1)"]
 name = "sqlalchemy2-stubs"
 version = "0.0.2a34"
 description = "Typing Stubs for SQLAlchemy 1.4"
-category = "main"
+category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1783,7 +1784,7 @@ typing-extensions = ">=3.7.4"
 name = "sqlmodel"
 version = "0.0.8"
 description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
-category = "main"
+category = "dev"
 optional = false
 python-versions = ">=3.6.1,<4.0.0"
 files = [
@@ -1798,14 +1799,14 @@ sqlalchemy2-stubs = "*"
 
 [[package]]
 name = "starlette"
-version = "0.26.1"
+version = "0.27.0"
 description = "The little ASGI library that shines."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"},
-    {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"},
+    {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"},
+    {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"},
 ]
 
 [package.dependencies]
@@ -1841,14 +1842,14 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.6.1"
+version = "4.6.2"
 description = "Backported and Experimental Type Hints for Python 3.7+"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "typing_extensions-4.6.1-py3-none-any.whl", hash = "sha256:6bac751f4789b135c43228e72de18637e9a6c29d12777023a703fd1a6858469f"},
-    {file = "typing_extensions-4.6.1.tar.gz", hash = "sha256:558bc0c4145f01e6405f4a5fdbd82050bd221b119f4bf72a961a1cfd471349d6"},
+    {file = "typing_extensions-4.6.2-py3-none-any.whl", hash = "sha256:3a8b36f13dd5fdc5d1b16fe317f5668545de77fa0b8e02006381fd49d731ab98"},
+    {file = "typing_extensions-4.6.2.tar.gz", hash = "sha256:06006244c70ac8ee83fa8282cb188f697b8db25bc8b4df07be1873c43897060c"},
 ]
 
 [[package]]
@@ -1907,4 +1908,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "668600818f0e41c8fcea2bf9f4752197333ccecdb7c15ca96b9d2da86b6d7611"
+content-hash = "98f9e4a28968a155c038521f5d337d0ad09be32f5bd24a8294ff150d2b02b263"
diff --git a/pyproject.toml b/pyproject.toml
index c6c08b622..6a8692fd7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,10 +21,10 @@ rich = "^12.5.1"
 httpx = "^0.23.0"
 PyJWT = "^2.4.0"
 pydantic = ">=1.10.8"
-sqlmodel = "^0.0.8"
 fastapi-users = "^10.4.1"
 
 [tool.poetry.group.dev.dependencies]
+sqlmodel = "^0.0.8"
 devtools = "^0.9.0"
 pytest = "^7.1.2"
 bumpver = "^2022.1118"


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index f8de6c8fb..ea2c18a10 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -34,7 +34,7 @@ def event_loop():
 
 @pytest.fixture
 async def client():
-    async with AsyncClient() as client:
+    async with AsyncClient(timeout=10) as client:
         yield client
 
 
diff --git a/tests/data/import-export/workflow.json b/tests/data/import-export/workflow.json
index 65dd1c8b5..2f5648620 100644
--- a/tests/data/import-export/workflow.json
+++ b/tests/data/import-export/workflow.json
@@ -20,7 +20,7 @@
                 "meta": {},
                 "name": "dummy",
                 "output_type": "Any",
-                "source": "pip-local:fractal_tasks_dummy-0.1.0-py3-none-any.whl"
+                "source": "pip_local:fractal_tasks_dummy:0.1.0:::dummy"
             }
         },
         {
@@ -38,7 +38,7 @@
                 "meta": {"parallelization_level": "index"},
                 "name": "dummy parallel",
                 "output_type": "Any",
-                "source": "pip-local:fractal_tasks_dummy-0.1.0-py3-none-any.whl"
+                "source": "pip_local:fractal_tasks_dummy:0.1.0:::dummy_parallel"
             }
         }
     ]
diff --git a/tests/fixtures_testserver.py b/tests/fixtures_testserver.py
index 9d53786b5..90f4c4631 100644
--- a/tests/fixtures_testserver.py
+++ b/tests/fixtures_testserver.py
@@ -177,11 +177,16 @@ async def _job_factory(**job_args_override):
 @pytest.fixture
 async def user_factory(client_superuser, testserver):
     async def __register_user(
-        email: str, password: str, slurm_user: Optional[str] = None
+        email: str,
+        password: str,
+        slurm_user: Optional[str] = None,
+        username: Optional[str] = None,
     ):
         payload = dict(email=email, password=password)
         if slurm_user:
             payload["slurm_user"] = slurm_user
+        if username:
+            payload["username"] = username
         res = await client_superuser.post(
             f"{testserver}/auth/register",
             json=payload,
@@ -197,4 +202,5 @@ async def register_user(user_factory):
     return await user_factory(
         email=environ["FRACTAL_USER"],
         password=environ["FRACTAL_PASSWORD"],
+        username="some_username",
     )
diff --git a/tests/test_task.py b/tests/test_task.py
index 6abdedc95..fb7baddc7 100644
--- a/tests/test_task.py
+++ b/tests/test_task.py
@@ -1,4 +1,5 @@
 import asyncio
+import json
 import time
 from pathlib import Path
 
@@ -9,6 +10,41 @@
 COLLECTION_TIMEOUT = 15.0
 
 
+async def test_task_collection_command(register_user, invoke, caplog):
+    """
+    Test that all `task collect` options are correctly parsed and included in
+    the the payload for the API request.
+    """
+    PACKAGE = "devtools"
+    PACKAGE_VERSION = "0.11.0"
+    PYTHON_VERSION = "1.2"
+    PACKAGE_EXTRAS = "a,b,c"
+    with pytest.raises(SystemExit):
+        await invoke(
+            (
+                "task collect "
+                f"{PACKAGE} "
+                f"--package-version {PACKAGE_VERSION} "
+                f"--python-version {PYTHON_VERSION} "
+                f"--package-extras {PACKAGE_EXTRAS}"
+            )
+        )
+
+    # Check that payload was prepared correctly
+    log_lines = [record.message for record in caplog.records]
+    debug(log_lines)
+    payload_line = next(
+        line for line in log_lines if line.startswith("Original payload: ")
+    )
+    assert payload_line
+    payload = json.loads(payload_line.strip("Original payload: "))
+    debug(payload)
+    assert payload["package"] == PACKAGE
+    assert payload["package_version"] == PACKAGE_VERSION
+    assert payload["package_extras"] == PACKAGE_EXTRAS
+    assert payload["python_version"] == PYTHON_VERSION
+
+
 async def test_task_collection_and_list(register_user, invoke, testdata_path):
     """
     GIVEN a pip installable package containing fractal-compatible tasks
@@ -32,6 +68,8 @@ async def test_task_collection_and_list(register_user, invoke, testdata_path):
     state_id = res0.data["id"]
     debug(state_id)
 
+    time.sleep(0.5)
+
     # Wait until collection is complete
     starting_time = time.perf_counter()
     while True:
@@ -86,6 +124,8 @@ async def test_repeated_task_collection(register_user, invoke, testdata_path):
     debug(venv_path)
     debug(state_id)
 
+    time.sleep(0.5)
+
     # Wait until collection is complete
     starting_time = time.perf_counter()
     while True:
@@ -104,14 +144,16 @@ async def test_repeated_task_collection(register_user, invoke, testdata_path):
 async def test_task_new(register_user, invoke):
 
     # create a new task with just positional required args
-    res = await invoke("task new _name _command _source")
+    res = await invoke("task new _name _command _source --version _version")
     res.show()
     assert res.retcode == 0
     assert res.data["name"] == "_name"
     assert res.data["command"] == "_command"
     assert res.data["source"] == "_source"
     assert res.data["input_type"] == res.data["output_type"] == "Any"
+    assert res.data["version"] == "_version"
     assert res.data["default_args"] == res.data["meta"] == {}
+    assert "owner" in res.data.keys()
     first_task_id = int(res.data["id"])
 
     # create a new task with batch option
@@ -160,6 +202,9 @@ async def test_task_edit(
     res = await invoke_as_superuser(f"task edit {task_id} --output-type {NEW}")
     assert res.data["output_type"] == NEW
     assert res.retcode == 0
+    res = await invoke_as_superuser(f"task edit {task_id} --version {NEW}")
+    assert res.data["version"] == NEW
+    assert res.retcode == 0
 
     # Test `file not found` errors
     with pytest.raises(FileNotFoundError):
diff --git a/tests/test_user.py b/tests/test_user.py
index fc9c998cf..3bec9eeda 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -27,7 +27,7 @@ async def test_register_as_superuser(invoke_as_superuser, is_superuser: bool):
     else:
         res = await invoke_as_superuser(
             f"user register {EMAIL_USER} {PWD_USER} "
-            "--slurm-user SOMETHING --cache-dir /absolute"
+            "--slurm-user SOMETHING --cache-dir /absolute --username X"
         )
         debug(res.data)
         assert res.retcode == 0
@@ -114,11 +114,13 @@ async def test_edit_as_superuser(invoke_as_superuser, new_is_superuser):
     NEW_EMAIL = "asd@asd.new"
     NEW_CACHE_DIR = "/tmp/xxx"
     NEW_SLURM_USER = "new_slurm"
+    NEW_USERNAME = "new_username"
     cmd = (
         f"user edit {user_id} "
         f"--new-email {NEW_EMAIL} "
         f"--new-password SOMETHING "
         f"--new-slurm-user {NEW_SLURM_USER} "
+        f"--new-username {NEW_USERNAME} "
         f"--new-cache-dir {NEW_CACHE_DIR}"
     )
     if new_is_superuser:
@@ -130,6 +132,7 @@ async def test_edit_as_superuser(invoke_as_superuser, new_is_superuser):
     assert res.data["email"] == NEW_EMAIL
     assert res.data["cache_dir"] == NEW_CACHE_DIR
     assert res.data["slurm_user"] == NEW_SLURM_USER
+    assert res.data["username"] == NEW_USERNAME
     assert res.data["is_superuser"] == new_is_superuser
 
     BAD_CACHE_DIR = "not_absolute"
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index b06a545a6..d44081bcf 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -363,67 +363,53 @@ async def test_workflow_apply(
     WHEN the client requests to apply the workflow to the project
     THEN the workflow is scheduled and executed, and the artifacts created
     """
+
+    # Collect tasks
     PACKAGE_NAME = testdata_path / "fractal_tasks_dummy-0.1.0-py3-none-any.whl"
     WORKFLOW_NAME = "mywf"
     DATASET_NAME = "myds"
-
     res0 = await invoke(f"task collect {PACKAGE_NAME}")
     debug(res0)
-    res0.show()
     venv_path = res0.data["data"]["venv_path"]
     debug(venv_path)
     state_id = res0.data["id"]
 
+    # Wait for task collection to end
     starting_time = time.perf_counter()
     while True:
         res1 = await invoke(f"task check-collection {state_id}")
         if res1.data["data"]["status"] == "OK":
             debug(res1.data)
             break
-        await asyncio.sleep(1)
+        time.sleep(1)
         assert time.perf_counter() - starting_time < TIMEOUT
 
+    # Create a project
     res = await invoke("project new testproject")
-    debug(res)
     assert res.retcode == 0
-
     prj = res.data
     prj_id = prj["id"]
     input_dataset_id = prj["dataset_list"][0]["id"]
 
+    # Create output dataset
     res = await invoke(f"project add-dataset {prj_id} {DATASET_NAME}")
     assert res.retcode == 0
     dataset = res.data
-    debug(dataset)
     output_dataset_id = dataset["id"]
 
-    res = await invoke(
-        f"dataset add-resource {prj_id} {input_dataset_id} {str(tmp_path)}"
-    )
-    resource = res.data
-    debug(resource)
-    assert res.retcode == 0
-
-    res = await invoke(
-        f"dataset add-resource {prj_id} {output_dataset_id} {str(tmp_path)}"
-    )
-    resource = res.data
-    debug(resource)
-    assert res.retcode == 0
-
-    # res = await invoke(
-    #     f"dataset add-resource {prj_id} {output_dataset_id} {str(tmp_path)}"
-    # )
-    # resource = res.data
-    # debug(resource)
-    # assert res.retcode == 0
+    # Add resources to datasets
+    for dataset_id in (input_dataset_id, output_dataset_id):
+        res = await invoke(
+            f"dataset add-resource {prj_id} {dataset_id} {str(tmp_path)}"
+        )
+        assert res.retcode == 0
 
+    # Create workflow and add task
     res = await invoke(f"workflow new {WORKFLOW_NAME} {prj_id}")
     workflow = res.data
     workflow_id = workflow["id"]
     debug(workflow)
     assert res.retcode == 0
-
     TASK_ID = 1
     res = await invoke(f"workflow add-task {prj_id} {workflow_id} {TASK_ID}")
     workflow_task = res.data
@@ -432,6 +418,7 @@ async def test_workflow_apply(
     TASK_NAME = res.data["task"]["name"]
     debug(TASK_NAME)
 
+    # Call `workflow apply`
     cmd = (
         f"workflow apply "
         f"{prj_id} {workflow_id} {input_dataset_id} {output_dataset_id}"
@@ -444,7 +431,8 @@ async def test_workflow_apply(
     job_id = job["id"]
     assert job["status"] == "submitted"
 
-    # TODO: add an assertion about the output, instead of calling `job show`
+    # Avoid immediately calling `job show` right after `workflow apply`
+    time.sleep(1)
 
     # Check that job completed successfully
     cmd = f"job show {prj_id} {job_id}"
@@ -459,10 +447,9 @@ async def test_workflow_apply(
             break
         elif job["status"] == "failed":
             raise RuntimeError(job)
-        await asyncio.sleep(1)
+        time.sleep(1)
         assert time.perf_counter() - starting_time < TIMEOUT
-    debug(res.data)
-    assert res.data["history"][0] == TASK_NAME
+    assert job["history"][0] == TASK_NAME
 
     # Prepare and run a workflow with a failing task
     args_file = str(tmp_path / "args.json")
@@ -482,21 +469,24 @@ async def test_workflow_apply(
     assert res.retcode == 0
     job_id = res.data["id"]
 
+    # Avoid immediately calling `job show` right after `workflow apply`
+    time.sleep(1)
+
     # Verify that status is failed, and that there is a log
     cmd = f"job show {prj_id} {job_id} --do-not-separate-logs"
     starting_time = time.perf_counter()
     while True:
         res = await invoke(cmd)
-        debug(res.data)
+        job = res.data
+        debug(job)
         assert res.retcode == 0
-        if res.data["status"] == "failed":
+        if job["status"] == "failed":
             break
-        await asyncio.sleep(1)
+        time.sleep(1)
         assert time.perf_counter() - starting_time < TIMEOUT
-    debug(res.data)
-    assert res.data["log"] is not None
+    assert job["log"] is not None
     # Note: the failing task is not added to the history
-    assert len(res.data["history"]) > 0
+    assert len(job["history"]) > 0
 
 
 async def test_workflow_export(