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

Issue:
Add `fractal whoami` command
See this test on the server:
```python
from devtools import debug


PREFIX = "/auth"


async def test_me(app, client, MockCurrentUser):
    # Anonymous
    res = await client.get(f"{PREFIX}/users/me")
    debug(res.json())
    assert res.status_code == 401
```
Add `fractal whoami` command
See this test on the server:
```python
from devtools import debug


PREFIX = "/auth"


async def test_me(app, client, MockCurrentUser):
    # Anonymous
    res = await client.get(f"{PREFIX}/users/me")
    debug(res.json())
    assert res.status_code == 401
```
Make `slurm_user` optional in config
See
```python
            async with AuthClient(
                username=args.user or settings.FRACTAL_USER,
                password=args.password or settings.FRACTAL_PASSWORD,
                slurm_user=args.slurm_user or settings.SLURM_USER,
            ) as client:
                interface = await handler(client, **vars(args))
```
where now in config we have
```python
    SLURM_USER: str = fail_getenv("SLURM_USER")
```

This should all work also with a `None`


Discussion / hints:
- [ ] Add parser (with zero arguments) to parser.py (see `register`, for instance)
- [ ] Add `whoami` to `cmd/__init__.py` (see `job`, but without `subcmd`). Since the code is very short, no need for an additional function (see `version`)
- [ ] Add a test
Ref:
* https://fastapi-users.github.io/fastapi-users/10.2/usage/routes/#get-me
- [ ] Add parser (with zero arguments) to parser.py (see `register`, for instance)
- [ ] Add `whoami` to `cmd/__init__.py` (see `job`, but without `subcmd`). Since the code is very short, no need for an additional function (see `version`)
- [ ] Add a test
Ref:
* https://fastapi-users.github.io/fastapi-users/10.2/usage/routes/#get-me


Fix patch:
diff --git a/docs/source/cli.rst b/docs/source/cli.rst
index 6503621e0..f98ec0211 100644
--- a/docs/source/cli.rst
+++ b/docs/source/cli.rst
@@ -12,7 +12,7 @@ Command-line interface for Fractal Client
    :maxdepth: 1
 
    fractal version <cli_files/version>
-   fractal register  <cli_files/register>
+   fractal user <cli_files/user>
    fractal project <cli_files/project>
    fractal dataset <cli_files/dataset>
    fractal workflow <cli_files/workflow>
diff --git a/docs/source/cli_files/register.rst b/docs/source/cli_files/user.rst
similarity index 60%
rename from docs/source/cli_files/register.rst
rename to docs/source/cli_files/user.rst
index 545599df9..3a00d58c2 100644
--- a/docs/source/cli_files/register.rst
+++ b/docs/source/cli_files/user.rst
@@ -1,8 +1,8 @@
-fractal register
-===================
+fractal user
+============
 
 .. argparse::
    :filename: fractal/parser.py
    :func: parser_main
    :prog: fractal
-   :path: register
+   :path: user
diff --git a/fractal/authclient.py b/fractal/authclient.py
index 65e957f30..70301f8db 100644
--- a/fractal/authclient.py
+++ b/fractal/authclient.py
@@ -17,12 +17,10 @@ def __init__(
         client: AsyncClient,
         username: str,
         password: str,
-        slurm_user: str,
     ):
         self.client = client
         self.username = username
         self.password = password
-        self.slurm_user = slurm_user
 
         try:
             with open(f"{settings.FRACTAL_CACHE_PATH}/session", "r") as f:
@@ -34,7 +32,6 @@ async def _get_fresh_token(self):
         data = dict(
             username=self.username,
             password=self.password,
-            slurm_user=self.slurm_user,
         )
         res = await self.client.post(
             f"{settings.FRACTAL_SERVER}/auth/token/login", data=data
@@ -88,13 +85,11 @@ def __init__(
         self,
         username: str,
         password: str,
-        slurm_user: str,
     ):
         self.auth = None
         self.client = None
         self.username = username
         self.password = password
-        self.slurm_user = slurm_user
 
     async def __aenter__(self):
         self.client = AsyncClient()
@@ -102,7 +97,6 @@ async def __aenter__(self):
             client=self.client,
             username=self.username,
             password=self.password,
-            slurm_user=self.slurm_user,
         )
         return self
 
diff --git a/fractal/client.py b/fractal/client.py
index 32f2a5012..50e747986 100644
--- a/fractal/client.py
+++ b/fractal/client.py
@@ -47,16 +47,18 @@ async def handle(cli_args: List[str] = argv):
         exit(1)
 
     try:
-        if args.cmd in ["version", "register"]:
+        kwargs = vars(args)
+        if args.cmd == "version":
             async with AsyncClient() as client:
-                interface = await handler(client, **vars(args))
+                interface = await handler(client, **kwargs)
         else:
+            # Extract (and remove) username/password for AuthClient from kwargs
+            username = kwargs.pop("user") or settings.FRACTAL_USER
+            password = kwargs.pop("password") or settings.FRACTAL_PASSWORD
             async with AuthClient(
-                username=args.user or settings.FRACTAL_USER,
-                password=args.password or settings.FRACTAL_PASSWORD,
-                slurm_user=args.slurm_user or settings.SLURM_USER,
+                username=username, password=password
             ) as client:
-                interface = await handler(client, **vars(args))
+                interface = await handler(client, **kwargs)
     except AuthenticationError as e:
         return PrintInterface(retcode=1, data=e.args[0])
     except ConnectError as e:
diff --git a/fractal/cmd/__init__.py b/fractal/cmd/__init__.py
index 458b827b8..13050d422 100644
--- a/fractal/cmd/__init__.py
+++ b/fractal/cmd/__init__.py
@@ -5,8 +5,6 @@
 from ..config import settings
 from ..interface import BaseInterface
 from ..interface import PrintInterface
-from ..interface import RichJsonInterface
-from ..response import check_response
 from ._dataset import dataset_add_resource
 from ._dataset import dataset_delete
 from ._dataset import dataset_delete_resource
@@ -25,6 +23,12 @@
 from ._task import task_delete
 from ._task import task_edit
 from ._task import task_list
+from ._user import user_delete
+from ._user import user_edit
+from ._user import user_list
+from ._user import user_register
+from ._user import user_show
+from ._user import user_whoami
 from ._workflow import workflow_add_task
 from ._workflow import workflow_apply
 from ._workflow import workflow_delete
@@ -108,42 +112,6 @@ async def dataset(
     return iface
 
 
-async def register(
-    client: AsyncClient,
-    email: str,
-    slurm_user: str,
-    password: str = None,
-    **kwargs,
-) -> BaseInterface:
-    from getpass import getpass
-
-    if password is None:
-        password = getpass()
-        confirm_password = getpass("Confirm password: ")
-        if password == confirm_password:
-            res = await client.post(
-                f"{settings.FRACTAL_SERVER}/auth/register",
-                json=dict(
-                    email=email, slurm_user=slurm_user, password=password
-                ),
-            )
-
-            data = check_response(res, expected_status_code=201)
-            iface = RichJsonInterface(retcode=0, data=data)
-        else:
-            iface = PrintInterface(retcode=1, data="Passwords do not match.")
-        return iface
-
-    res = await client.post(
-        f"{settings.FRACTAL_SERVER}/auth/register",
-        json=dict(email=email, slurm_user=slurm_user, password=password),
-    )
-    data = check_response(res, expected_status_code=201)
-    iface = RichJsonInterface(retcode=0, data=data)
-
-    return iface
-
-
 async def task(
     client: AuthClient, subcmd: str, batch: bool = False, **kwargs
 ) -> BaseInterface:
@@ -216,3 +184,22 @@ async def version(client: AsyncClient, **kwargs) -> PrintInterface:
             f"\tversion: {data['version']}"
         ),
     )
+
+
+async def user(client: AuthClient, subcmd: str, **kwargs) -> BaseInterface:
+    if subcmd == "register":
+        iface = await user_register(client, **kwargs)
+    elif subcmd == "list":
+        iface = await user_list(client, **kwargs)
+    elif subcmd == "show":
+        iface = await user_show(client, **kwargs)
+    elif subcmd == "edit":
+        iface = await user_edit(client, **kwargs)
+    elif subcmd == "delete":
+        iface = await user_delete(client, **kwargs)
+    elif subcmd == "whoami":
+        iface = await user_whoami(client, **kwargs)
+    else:
+        raise NoCommandError(f"Command user {subcmd} not found")
+
+    return iface
diff --git a/fractal/cmd/_user.py b/fractal/cmd/_user.py
new file mode 100644
index 000000000..d23b5e6f0
--- /dev/null
+++ b/fractal/cmd/_user.py
@@ -0,0 +1,128 @@
+from typing import Optional
+from typing import Union
+
+from ..authclient import AuthClient
+from ..common.schemas.user import UserCreate
+from ..common.schemas.user import UserRead
+from ..common.schemas.user import UserUpdate
+from ..config import settings
+from ..interface import PrintInterface
+from ..interface import RichJsonInterface
+from ..response import check_response
+
+
+async def user_register(
+    client: AuthClient,
+    new_email: str,
+    new_password: Optional[str] = None,
+    slurm_user: Optional[str] = None,
+    superuser: bool = False,
+    **kwargs,
+) -> Union[RichJsonInterface, PrintInterface]:
+
+    new_user = UserCreate(
+        email=new_email, password=new_password, slurm_user=slurm_user
+    )
+
+    from getpass import getpass
+
+    if new_password is None:
+        new_password = getpass()
+        confirm_new_password = getpass("Confirm password: ")
+        if new_password == confirm_new_password:
+            new_user.password = new_password
+        else:
+            iface = PrintInterface(retcode=1, data="Passwords do not match.")
+            return iface
+
+    res = await client.post(
+        f"{settings.FRACTAL_SERVER}/auth/register",
+        json=new_user.dict(exclude_unset=True, exclude_none=True),
+    )
+    data = check_response(res, expected_status_code=201, coerce=UserRead)
+
+    if superuser:
+        user_id = data.id
+        res = await client.patch(
+            f"{settings.FRACTAL_SERVER}/auth/users/{user_id}",
+            json={"is_superuser": True},
+        )
+        data = check_response(res, expected_status_code=200, coerce=UserRead)
+
+    iface = RichJsonInterface(retcode=0, data=data.dict())
+
+    return iface
+
+
+async def user_list(client: AuthClient, **kwargs) -> RichJsonInterface:
+    res = await client.get(f"{settings.FRACTAL_SERVER}/auth/userlist")
+    users = check_response(res, expected_status_code=200)
+    users = [UserRead(**user).dict() for user in users]
+    return RichJsonInterface(
+        retcode=0,
+        data=users,
+    )
+
+
+async def user_show(
+    client: AuthClient, user_id: str, **kwargs
+) -> RichJsonInterface:
+    res = await client.get(f"{settings.FRACTAL_SERVER}/auth/users/{user_id}")
+    user = check_response(res, expected_status_code=200, coerce=UserRead)
+    return RichJsonInterface(
+        retcode=0,
+        data=user.dict(),
+    )
+
+
+async def user_edit(
+    client: AuthClient,
+    user_id: str,
+    new_email: Optional[str] = None,
+    new_slurm_user: Optional[str] = None,
+    make_superuser: bool = False,
+    remove_superuser: bool = False,
+    **kwargs,
+) -> Union[RichJsonInterface, PrintInterface]:
+
+    user_update = UserUpdate(
+        email=new_email,
+        slurm_user=new_slurm_user,
+    )
+
+    if make_superuser:
+        user_update.is_superuser = True
+    if remove_superuser:
+        user_update.is_superuser = False
+
+    payload = user_update.dict(exclude_unset=True, exclude_none=True)
+    if not payload:
+        return PrintInterface(retcode=1, data="Nothing to update")
+
+    res = await client.patch(
+        f"{settings.FRACTAL_SERVER}/auth/users/{user_id}", json=payload
+    )
+    new_user = check_response(res, expected_status_code=200, coerce=UserRead)
+
+    return RichJsonInterface(
+        retcode=0,
+        data=new_user.dict(),
+    )
+
+
+async def user_delete(
+    client: AuthClient, user_id: str, **kwargs
+) -> PrintInterface:
+
+    res = await client.delete(
+        f"{settings.FRACTAL_SERVER}/auth/users/{user_id}"
+    )
+    check_response(res, expected_status_code=204)
+    return PrintInterface(retcode=0, data="")
+
+
+async def user_whoami(client: AuthClient, **kwargs) -> RichJsonInterface:
+    res = await client.get(f"{settings.FRACTAL_SERVER}/auth/whoami")
+    user = check_response(res, expected_status_code=200, coerce=UserRead)
+
+    return RichJsonInterface(retcode=0, data=user.dict())
diff --git a/fractal/cmd/utils.py b/fractal/cmd/utils.py
index 42026e18a..4d96c6468 100644
--- a/fractal/cmd/utils.py
+++ b/fractal/cmd/utils.py
@@ -1,6 +1,8 @@
 import json
 import logging
 from pathlib import Path
+from typing import Any
+from typing import Dict
 from typing import List
 
 from ..authclient import AuthClient
@@ -42,7 +44,9 @@ async def get_cached_task_by_name(name: str, client: AuthClient) -> int:
             raise KeyError(f'Task "{name}" not in {cache_file}\n', str(e))
 
 
-async def refresh_task_cache(client: AuthClient, **kwargs) -> List[dict]:
+async def refresh_task_cache(
+    client: AuthClient, **kwargs
+) -> List[Dict[str, Any]]:
 
     # Get task_list
     res = await client.get(f"{settings.BASE_URL}/task/")
diff --git a/fractal/common b/fractal/common
index 80213f201..cab7c8943 160000
--- a/fractal/common
+++ b/fractal/common
@@ -1,1 +1,1 @@
-Subproject commit 80213f2011a903afb0233fef0169730991a715a3
+Subproject commit cab7c894372d410262befd457bec35d239675eb1
diff --git a/fractal/config.py b/fractal/config.py
index d33c733a6..605150c9e 100644
--- a/fractal/config.py
+++ b/fractal/config.py
@@ -4,6 +4,7 @@
 
 Original authors:
 Jacopo Nespolo <jacopo.nespolo@exact-lab.it>
+Tommaso Comparin <tommaso.comparin@exact-lab.it>
 
 This file is part of Fractal and was originally developed by eXact lab S.r.l.
 <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
@@ -12,6 +13,7 @@
 """
 import logging
 from os import getenv
+from typing import Optional
 
 from dotenv import load_dotenv
 from pydantic import BaseSettings
@@ -37,7 +39,7 @@ class Settings(BaseSettings):
 
     FRACTAL_USER: str = fail_getenv("FRACTAL_USER")
     FRACTAL_PASSWORD: str = fail_getenv("FRACTAL_PASSWORD")
-    SLURM_USER: str = fail_getenv("SLURM_USER")
+    SLURM_USER: Optional[str] = getenv("SLURM_USER")
 
     FRACTAL_SERVER: str = getenv("FRACTAL_SERVER", "http://localhost:8000")
 
diff --git a/fractal/interface.py b/fractal/interface.py
index 909b56a67..8f13b3ba2 100644
--- a/fractal/interface.py
+++ b/fractal/interface.py
@@ -1,7 +1,9 @@
 from typing import Any
 from typing import Dict
+from typing import List
 from typing import Optional
 from typing import Sequence
+from typing import Union
 
 from rich import print_json
 from rich.console import Console
@@ -39,7 +41,7 @@ class RichJsonInterface(BaseInterface):
     def __init__(
         self,
         retcode: int,
-        data: Dict[str, Any],
+        data: Union[Dict[str, Any], List[Dict[str, Any]]],
         extra_lines: Optional[Sequence[str]] = None,
     ):
         super().__init__(retcode, data)
diff --git a/fractal/parser.py b/fractal/parser.py
index 05a5ddd5a..9c98fbe69 100644
--- a/fractal/parser.py
+++ b/fractal/parser.py
@@ -6,6 +6,7 @@
 Jacopo Nespolo <jacopo.nespolo@exact-lab.it>
 Marco Franzon <marco.franzon@exact-lab.it>
 Tommaso Comparin <tommaso.comparin@exact-lab.it>
+Yuri Chiucconi  <yuri.chiucconi@exact-lab.it>
 
 This file is part of Fractal and was originally developed by eXact lab S.r.l.
 <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
@@ -55,21 +56,6 @@
 subparsers_main = parser_main.add_subparsers(title="Commands:", dest="cmd")
 
 
-# REGISTER GROUP
-register_parser = subparsers_main.add_parser(
-    "register", description="Register with the Fractal server"
-)
-register_parser.add_argument("email", help="Email to be used as username")
-register_parser.add_argument(
-    "slurm_user", help="Username to login into Slurm cluster"
-)
-register_parser.add_argument(
-    "-p",
-    "--password",
-    help=("Password for the new user"),
-)
-
-
 # PROJECT GROUP
 project_parser = subparsers_main.add_parser(
     "project", description="Project commands"
@@ -499,6 +485,7 @@
     dest="do_not_separate_logs",
     help="Show the job logs in the main output, instead of a separate field",
     action="store_true",
+    required=False,
 )
 
 # job download-logs
@@ -522,3 +509,86 @@
 version_parser = subparsers_main.add_parser(
     "version", description="Print version and exit"
 )
+
+
+# USER GROUP
+
+user_parser = subparsers_main.add_parser(
+    "user",
+    description="User commands",
+)
+user_subparsers = user_parser.add_subparsers(
+    title="Valid subcommand", dest="subcmd", required=True
+)
+
+# user whoami
+user_whoami_parser = user_subparsers.add_parser(
+    "whoami",
+    description="Get info on current user (fails if user is not registered)",
+)
+
+# user register
+user_register_parser = user_subparsers.add_parser(
+    "register", description="Register a new user with the Fractal server"
+)
+user_register_parser.add_argument(
+    "new_email", help="Email to be used as username"
+)
+user_register_parser.add_argument(
+    "new_password", help="Password for the new user"
+)
+user_register_parser.add_argument(
+    "--slurm_user",
+    help="Username to login into Slurm cluster",
+    required=False,
+)
+user_register_parser.add_argument(
+    "--superuser",
+    help="Give superuser privileges to the new user",
+    action="store_true",
+    required=False,
+)
+
+# user list
+user_list_parser = user_subparsers.add_parser(
+    "list",
+    description="List all users",
+)
+
+# user show
+user_show_parser = user_subparsers.add_parser(
+    "show", description="Show details of single user"
+)
+user_show_parser.add_argument("user_id", help="ID of the user")
+
+# user edit
+user_edit_parser = user_subparsers.add_parser(
+    "edit", description="Edit details of single user"
+)
+user_edit_parser.add_argument("user_id", help="ID of the user")
+user_edit_parser.add_argument(
+    "--new-email", help="New email address", type=str, required=False
+)
+user_edit_parser.add_argument(
+    "--new-slurm-user", help="New SLURM username", type=str, required=False
+)
+
+user_edit_parser_superuser = user_edit_parser.add_mutually_exclusive_group()
+user_edit_parser_superuser.add_argument(
+    "--make-superuser",
+    help="Give superuser privileges to user",
+    action="store_true",
+    required=False,
+)
+user_edit_parser_superuser.add_argument(
+    "--remove-superuser",
+    help="Remove superuser privileges from user",
+    action="store_true",
+    required=False,
+)
+
+# user delete
+user_delete_parser = user_subparsers.add_parser(
+    "delete", description="Delete a single user"
+)
+user_delete_parser.add_argument("user_id", help="ID of the user")
diff --git a/poetry.lock b/poetry.lock
index 0808b2553..458868e75 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -17,26 +17,26 @@ typing_extensions = ">=3.7.2"
 
 [[package]]
 name = "alabaster"
-version = "0.7.12"
+version = "0.7.13"
 description = "A configurable sidebar-enabled Sphinx theme"
 category = "dev"
 optional = false
-python-versions = "*"
+python-versions = ">=3.6"
 files = [
-    {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"},
-    {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
+    {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"},
+    {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"},
 ]
 
 [[package]]
 name = "alembic"
-version = "1.9.1"
+version = "1.9.2"
 description = "A database migration tool for SQLAlchemy."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "alembic-1.9.1-py3-none-any.whl", hash = "sha256:a9781ed0979a20341c2cbb56bd22bd8db4fc1913f955e705444bd3a97c59fa32"},
-    {file = "alembic-1.9.1.tar.gz", hash = "sha256:f9f76e41061f5ebe27d4fe92600df9dd612521a7683f904dab328ba02cffa5a2"},
+    {file = "alembic-1.9.2-py3-none-any.whl", hash = "sha256:e8a6ff9f3b1887e1fed68bfb8fb9a000d8f61c21bdcc85b67bb9f87fcbc4fce3"},
+    {file = "alembic-1.9.2.tar.gz", hash = "sha256:6880dec4f28dd7bd999d2ed13fbe7c9d4337700a44d11a524c0ce0c59aaf0dbd"},
 ]
 
 [package.dependencies]
@@ -267,19 +267,102 @@ pycparser = "*"
 
 [[package]]
 name = "charset-normalizer"
-version = "2.1.1"
+version = "3.0.1"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "dev"
 optional = false
-python-versions = ">=3.6.0"
+python-versions = "*"
 files = [
-    {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
-    {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
+    {file = "charset-normalizer-3.0.1.tar.gz", hash = "sha256:ebea339af930f8ca5d7a699b921106c6e29c617fe9606fa7baa043c1cdae326f"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88600c72ef7587fe1708fd242b385b6ed4b8904976d5da0893e31df8b3480cb6"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c75ffc45f25324e68ab238cb4b5c0a38cd1c3d7f1fb1f72b5541de469e2247db"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db72b07027db150f468fbada4d85b3b2729a3db39178abf5c543b784c1254539"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62595ab75873d50d57323a91dd03e6966eb79c41fa834b7a1661ed043b2d404d"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff6f3db31555657f3163b15a6b7c6938d08df7adbfc9dd13d9d19edad678f1e8"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:772b87914ff1152b92a197ef4ea40efe27a378606c39446ded52c8f80f79702e"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70990b9c51340e4044cfc394a81f614f3f90d41397104d226f21e66de668730d"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:292d5e8ba896bbfd6334b096e34bffb56161c81408d6d036a7dfa6929cff8783"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2edb64ee7bf1ed524a1da60cdcd2e1f6e2b4f66ef7c077680739f1641f62f555"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:31a9ddf4718d10ae04d9b18801bd776693487cbb57d74cc3458a7673f6f34639"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:44ba614de5361b3e5278e1241fda3dc1838deed864b50a10d7ce92983797fa76"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:12db3b2c533c23ab812c2b25934f60383361f8a376ae272665f8e48b88e8e1c6"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c512accbd6ff0270939b9ac214b84fb5ada5f0409c44298361b2f5e13f9aed9e"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-win32.whl", hash = "sha256:502218f52498a36d6bf5ea77081844017bf7982cdbe521ad85e64cabee1b608b"},
+    {file = "charset_normalizer-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:601f36512f9e28f029d9481bdaf8e89e5148ac5d89cffd3b05cd533eeb423b59"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0298eafff88c99982a4cf66ba2efa1128e4ddaca0b05eec4c456bbc7db691d8d"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8d0fc946c784ff7f7c3742310cc8a57c5c6dc31631269876a88b809dbeff3d3"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87701167f2a5c930b403e9756fab1d31d4d4da52856143b609e30a1ce7160f3c"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e76c0f23218b8f46c4d87018ca2e441535aed3632ca134b10239dfb6dadd6b"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0a590235ccd933d9892c627dec5bc7511ce6ad6c1011fdf5b11363022746c1"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c7fe7afa480e3e82eed58e0ca89f751cd14d767638e2550c77a92a9e749c317"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79909e27e8e4fcc9db4addea88aa63f6423ebb171db091fb4373e3312cb6d603"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ac7b6a045b814cf0c47f3623d21ebd88b3e8cf216a14790b455ea7ff0135d18"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:72966d1b297c741541ca8cf1223ff262a6febe52481af742036a0b296e35fa5a"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f9d0c5c045a3ca9bedfc35dca8526798eb91a07aa7a2c0fee134c6c6f321cbd7"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5995f0164fa7df59db4746112fec3f49c461dd6b31b841873443bdb077c13cfc"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4a8fcf28c05c1f6d7e177a9a46a1c52798bfe2ad80681d275b10dcf317deaf0b"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:761e8904c07ad053d285670f36dd94e1b6ab7f16ce62b9805c475b7aa1cffde6"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-win32.whl", hash = "sha256:71140351489970dfe5e60fc621ada3e0f41104a5eddaca47a7acb3c1b851d6d3"},
+    {file = "charset_normalizer-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ab77acb98eba3fd2a85cd160851816bfce6871d944d885febf012713f06659c"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:84c3990934bae40ea69a82034912ffe5a62c60bbf6ec5bc9691419641d7d5c9a"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74292fc76c905c0ef095fe11e188a32ebd03bc38f3f3e9bcb85e4e6db177b7ea"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c95a03c79bbe30eec3ec2b7f076074f4281526724c8685a42872974ef4d36b72"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c39b0e3eac288fedc2b43055cfc2ca7a60362d0e5e87a637beac5d801ef478"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df2c707231459e8a4028eabcd3cfc827befd635b3ef72eada84ab13b52e1574d"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93ad6d87ac18e2a90b0fe89df7c65263b9a99a0eb98f0a3d2e079f12a0735837"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:59e5686dd847347e55dffcc191a96622f016bc0ad89105e24c14e0d6305acbc6"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:cd6056167405314a4dc3c173943f11249fa0f1b204f8b51ed4bde1a9cd1834dc"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:083c8d17153ecb403e5e1eb76a7ef4babfc2c48d58899c98fcaa04833e7a2f9a"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:f5057856d21e7586765171eac8b9fc3f7d44ef39425f85dbcccb13b3ebea806c"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:7eb33a30d75562222b64f569c642ff3dc6689e09adda43a082208397f016c39a"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-win32.whl", hash = "sha256:95dea361dd73757c6f1c0a1480ac499952c16ac83f7f5f4f84f0658a01b8ef41"},
+    {file = "charset_normalizer-3.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:eaa379fcd227ca235d04152ca6704c7cb55564116f8bc52545ff357628e10602"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3e45867f1f2ab0711d60c6c71746ac53537f1684baa699f4f668d4c6f6ce8e14"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cadaeaba78750d58d3cc6ac4d1fd867da6fc73c88156b7a3212a3cd4819d679d"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:911d8a40b2bef5b8bbae2e36a0b103f142ac53557ab421dc16ac4aafee6f53dc"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:503e65837c71b875ecdd733877d852adbc465bd82c768a067badd953bf1bc5a3"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a60332922359f920193b1d4826953c507a877b523b2395ad7bc716ddd386d866"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:16a8663d6e281208d78806dbe14ee9903715361cf81f6d4309944e4d1e59ac5b"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a16418ecf1329f71df119e8a65f3aa68004a3f9383821edcb20f0702934d8087"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9d9153257a3f70d5f69edf2325357251ed20f772b12e593f3b3377b5f78e7ef8"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:02a51034802cbf38db3f89c66fb5d2ec57e6fe7ef2f4a44d070a593c3688667b"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:2e396d70bc4ef5325b72b593a72c8979999aa52fb8bcf03f701c1b03e1166918"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:11b53acf2411c3b09e6af37e4b9005cba376c872503c8f28218c7243582df45d"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:0bf2dae5291758b6f84cf923bfaa285632816007db0330002fa1de38bfcb7154"},
+    {file = "charset_normalizer-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2c03cc56021a4bd59be889c2b9257dae13bf55041a3372d3295416f86b295fb5"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:024e606be3ed92216e2b6952ed859d86b4cfa52cd5bc5f050e7dc28f9b43ec42"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4b0d02d7102dd0f997580b51edc4cebcf2ab6397a7edf89f1c73b586c614272c"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:358a7c4cb8ba9b46c453b1dd8d9e431452d5249072e4f56cfda3149f6ab1405e"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81d6741ab457d14fdedc215516665050f3822d3e56508921cc7239f8c8e66a58"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8af03d2e37866d023ad0ddea594edefc31e827fee64f8de5611a1dbc373174"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cf4e8ad252f7c38dd1f676b46514f92dc0ebeb0db5552f5f403509705e24753"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e696f0dd336161fca9adbb846875d40752e6eba585843c768935ba5c9960722b"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c22d3fe05ce11d3671297dc8973267daa0f938b93ec716e12e0f6dee81591dc1"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:109487860ef6a328f3eec66f2bf78b0b72400280d8f8ea05f69c51644ba6521a"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:37f8febc8ec50c14f3ec9637505f28e58d4f66752207ea177c1d67df25da5aed"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:f97e83fa6c25693c7a35de154681fcc257c1c41b38beb0304b9c4d2d9e164479"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a152f5f33d64a6be73f1d30c9cc82dfc73cec6477ec268e7c6e4c7d23c2d2291"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:39049da0ffb96c8cbb65cbf5c5f3ca3168990adf3551bd1dee10c48fce8ae820"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-win32.whl", hash = "sha256:4457ea6774b5611f4bed5eaa5df55f70abde42364d498c5134b7ef4c6958e20e"},
+    {file = "charset_normalizer-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:e62164b50f84e20601c1ff8eb55620d2ad25fb81b59e3cd776a1902527a788af"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8eade758719add78ec36dc13201483f8e9b5d940329285edcd5f70c0a9edbd7f"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8499ca8f4502af841f68135133d8258f7b32a53a1d594aa98cc52013fff55678"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3fc1c4a2ffd64890aebdb3f97e1278b0cc72579a08ca4de8cd2c04799a3a22be"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3ffdaafe92a5dc603cb9bd5111aaa36dfa187c8285c543be562e61b755f6b"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2ac1b08635a8cd4e0cbeaf6f5e922085908d48eb05d44c5ae9eabab148512ca"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6f45710b4459401609ebebdbcfb34515da4fc2aa886f95107f556ac69a9147e"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ae1de54a77dc0d6d5fcf623290af4266412a7c4be0b1ff7444394f03f5c54e3"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b590df687e3c5ee0deef9fc8c547d81986d9a1b56073d82de008744452d6541"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab5de034a886f616a5668aa5d098af2b5385ed70142090e2a31bcbd0af0fdb3d"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9cb3032517f1627cc012dbc80a8ec976ae76d93ea2b5feaa9d2a5b8882597579"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:608862a7bf6957f2333fc54ab4399e405baad0163dc9f8d99cb236816db169d4"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0f438ae3532723fb6ead77e7c604be7c8374094ef4ee2c5e03a3a17f1fca256c"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:356541bf4381fa35856dafa6a965916e54bed415ad8a24ee6de6e37deccf2786"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-win32.whl", hash = "sha256:39cf9ed17fe3b1bc81f33c9ceb6ce67683ee7526e65fde1447c772afc54a1bb8"},
+    {file = "charset_normalizer-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:0a11e971ed097d24c534c037d298ad32c6ce81a45736d31e0ff0ad37ab437d59"},
+    {file = "charset_normalizer-3.0.1-py3-none-any.whl", hash = "sha256:7e189e2e1d3ed2f4aebabd2d5b0f931e883676e51c7624826e0a4e5fe8a0bf24"},
 ]
 
-[package.extras]
-unicode-backport = ["unicodedata2"]
-
 [[package]]
 name = "click"
 version = "8.1.3"
@@ -453,22 +536,23 @@ pygments = ["pygments (>=2.2.0)"]
 
 [[package]]
 name = "dnspython"
-version = "2.2.1"
+version = "2.3.0"
 description = "DNS toolkit"
 category = "dev"
 optional = false
-python-versions = ">=3.6,<4.0"
+python-versions = ">=3.7,<4.0"
 files = [
-    {file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"},
-    {file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"},
+    {file = "dnspython-2.3.0-py3-none-any.whl", hash = "sha256:89141536394f909066cabd112e3e1a37e4e654db00a25308b0f130bc3152eb46"},
+    {file = "dnspython-2.3.0.tar.gz", hash = "sha256:224e32b03eb46be70e12ef6d64e0be123a64e621ab4c0822ff6d450d52a540b9"},
 ]
 
 [package.extras]
 curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"]
-dnssec = ["cryptography (>=2.6,<37.0)"]
-doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"]
+dnssec = ["cryptography (>=2.6,<40.0)"]
+doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.11.0)"]
+doq = ["aioquic (>=0.9.20)"]
 idna = ["idna (>=2.1,<4.0)"]
-trio = ["trio (>=0.14,<0.20)"]
+trio = ["trio (>=0.14,<0.23)"]
 wmi = ["wmi (>=1.5.1,<2.0.0)"]
 
 [[package]]
@@ -550,14 +634,14 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=
 
 [[package]]
 name = "fastapi-users"
-version = "10.2.1"
+version = "10.3.0"
 description = "Ready-to-use and customizable users management for FastAPI"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "fastapi_users-10.2.1-py3-none-any.whl", hash = "sha256:4805f352a74e038ad2bb6bdff516975cc67f860b15b82625b91375691de8494a"},
-    {file = "fastapi_users-10.2.1.tar.gz", hash = "sha256:b26a8ce043b6588c38c77266cc9faa623a3235740d22f4bc66488fc2709ab465"},
+    {file = "fastapi_users-10.3.0-py3-none-any.whl", hash = "sha256:54596acad3df21bc3a06b7e67584d0e087186d726b99c7629e82f66da94aea91"},
+    {file = "fastapi_users-10.3.0.tar.gz", hash = "sha256:ab9dd218db088bc2d04474ec5fd0040971cea027186041d6d33979f990917d9a"},
 ]
 
 [package.dependencies]
@@ -594,14 +678,14 @@ sqlmodel = "*"
 
 [[package]]
 name = "fractal-server"
-version = "1.0.0rc1"
+version = "1.0.0rc2"
 description = "Server component of the Fractal analytics platform"
 category = "dev"
 optional = false
 python-versions = ">=3.8,<4.0"
 files = [
-    {file = "fractal_server-1.0.0rc1-py3-none-any.whl", hash = "sha256:9a66d26b1c231afb3b52ca44a2f16aacc2ab47d2d5fe6e5342bec10dc85cb30d"},
-    {file = "fractal_server-1.0.0rc1.tar.gz", hash = "sha256:f6d017f6c609eb2a943dc17423abe67613609b87d965359394e19f504d4a0081"},
+    {file = "fractal_server-1.0.0rc2-py3-none-any.whl", hash = "sha256:a5f36058250be8911078848c25804fe48a6b5b85aecdbc17c0cb38b4fa18c1f0"},
+    {file = "fractal_server-1.0.0rc2.tar.gz", hash = "sha256:f7f623e297a19b8774a2134abb52ec33c9daada36ece5a40933d0d0d665c8381"},
 ]
 
 [package.dependencies]
@@ -931,52 +1015,62 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 
 [[package]]
 name = "markupsafe"
-version = "2.1.1"
+version = "2.1.2"
 description = "Safely add untrusted strings to HTML/XML markup."
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
-    {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
-    {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
-    {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
-    {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
-    {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"},
+    {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"},
+    {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"},
+    {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"},
+    {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"},
+    {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"},
+    {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"},
 ]
 
 [[package]]
@@ -1205,14 +1299,14 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
 
 [[package]]
 name = "pytest"
-version = "7.2.0"
+version = "7.2.1"
 description = "pytest: simple powerful testing with Python"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
-    {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
+    {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"},
+    {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"},
 ]
 
 [package.dependencies]
@@ -1276,14 +1370,14 @@ six = ">=1.4.0"
 
 [[package]]
 name = "pytz"
-version = "2022.7"
+version = "2022.7.1"
 description = "World timezone definitions, modern and historical"
 category = "dev"
 optional = false
 python-versions = "*"
 files = [
-    {file = "pytz-2022.7-py2.py3-none-any.whl", hash = "sha256:93007def75ae22f7cd991c84e02d434876818661f8df9ad5df9e950ff4e52cfd"},
-    {file = "pytz-2022.7.tar.gz", hash = "sha256:7ccfae7b4b2c067464a6733c6261673fdb8fd1be905460396b97a073e9fa683a"},
+    {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"},
+    {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"},
 ]
 
 [[package]]
@@ -1338,19 +1432,19 @@ files = [
 
 [[package]]
 name = "requests"
-version = "2.28.1"
+version = "2.28.2"
 description = "Python HTTP for Humans."
 category = "dev"
 optional = false
 python-versions = ">=3.7, <4"
 files = [
-    {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
-    {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
+    {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
+    {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
 ]
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-charset-normalizer = ">=2,<3"
+charset-normalizer = ">=2,<4"
 idna = ">=2.5,<4"
 urllib3 = ">=1.21.1,<1.27"
 
@@ -1398,18 +1492,18 @@ jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"]
 
 [[package]]
 name = "setuptools"
-version = "65.6.3"
+version = "66.0.0"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"},
-    {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"},
+    {file = "setuptools-66.0.0-py3-none-any.whl", hash = "sha256:a78d01d1e2c175c474884671dde039962c9d74c7223db7369771fcf6e29ceeab"},
+    {file = "setuptools-66.0.0.tar.gz", hash = "sha256:bd6eb2d6722568de6d14b87c44a96fac54b2a45ff5e940e639979a3d1792adb6"},
 ]
 
 [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-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+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-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"]
 
@@ -1858,4 +1952,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "7296fbdf075e4edaad52f5ad33c300b60c93d302353188b4d7f13e9cbefe9993"
+content-hash = "572de58112b0f984f78126c8d65046d30a42fc8c4322496588e1a7b52f3f8bd4"
diff --git a/pyproject.toml b/pyproject.toml
index ccee48969..d2a88b955 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 = ">=1.0.0rc1"
+fractal-server = ">=1.0.0rc2"
 coverage = {extras = ["toml"], version = "^6.5.0"}
 
 [tool.poetry.group.docs.dependencies]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 3e7f5fc5d..2a4c77e06 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,4 @@
+import multiprocessing
 import shlex
 from os import environ
 from pathlib import Path
@@ -12,6 +13,11 @@
 environ["DB_ECHO"] = "0"
 environ["SLURM_USER"] = "slurm_user"
 
+# set_start_method("fork") necessary to run tests on MacOS
+# https://github.com/pytest-dev/pytest-flask/issues/104#issuecomment-577908228
+# https://docs.python.org/3/library/multiprocessing.html#multiprocessing.get_start_method
+multiprocessing.set_start_method("fork")
+
 
 @pytest.fixture(scope="session")
 def testdata_path() -> Path:
@@ -33,6 +39,17 @@ async def client():
         yield client
 
 
+@pytest.fixture
+async def client_superuser():
+    from fractal.authclient import AuthClient
+
+    async with AuthClient(
+        username="admin@fractal.xy",
+        password="1234",
+    ) as client_superuser:
+        yield client_superuser
+
+
 @pytest.fixture(scope="session")
 def clisplit():
     def __clisplit(args: str):
@@ -51,6 +68,17 @@ async def __invoke(args: str):
     return __invoke
 
 
+@pytest.fixture
+async def invoke_as_superuser(clisplit):
+    from fractal.client import handle
+
+    async def __invoke(args: str):
+        new_args = f"--user admin@fractal.xy --password 1234 {args}"
+        return await handle(clisplit(new_args))
+
+    return __invoke
+
+
 @pytest.fixture
 def clear_task_cache():
     from fractal.config import settings
diff --git a/tests/fixtures_testserver.py b/tests/fixtures_testserver.py
index 85728af01..596fcb224 100644
--- a/tests/fixtures_testserver.py
+++ b/tests/fixtures_testserver.py
@@ -44,6 +44,7 @@ def _get_settings():
 async def testserver(override_server_settings):
     import uvicorn
     from fractal_server.main import start_application
+    from fractal_server.main import _create_user
     from multiprocessing import Process
     from fractal_server.app.db import DB
     from fractal_server.app.models import SQLModel
@@ -56,6 +57,7 @@ async def testserver(override_server_settings):
     # We are explicitly calling start_application() to bypass the task
     # collection routine
     app = start_application()
+    await _create_user("admin@fractal.xy", "1234", is_superuser=True)
 
     config = uvicorn.Config(app, port=10080, log_level="debug")
     server = uvicorn.Server(config)
@@ -175,9 +177,9 @@ async def _job_factory(**job_args_override):
 
 
 @pytest.fixture
-async def user_factory(client, testserver):
+async def user_factory(client_superuser, testserver):
     async def __register_user(email: str, password: str, slurm_user: str):
-        res = await client.post(
+        res = await client_superuser.post(
             f"{testserver}/auth/register",
             json=dict(email=email, password=password, slurm_user=slurm_user),
         )
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 9129b1c05..1e8bd0c6e 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -17,7 +17,6 @@ async def test_auth_fail(client):
             client,
             username=environ.get("FRACTAL_USER"),
             password=environ.get("FRACTAL_PASSWORD"),
-            slurm_user=environ.get("SLURM_USER"),
         )
         await auth()
 
@@ -32,7 +31,6 @@ async def test_auth_registered(client, register_user):
         client,
         username=environ.get("FRACTAL_USER"),
         password=environ.get("FRACTAL_PASSWORD"),
-        slurm_user=environ.get("SLURM_USER"),
     )
     token = await auth()
     assert token
diff --git a/tests/test_user.py b/tests/test_user.py
new file mode 100644
index 000000000..44f276272
--- /dev/null
+++ b/tests/test_user.py
@@ -0,0 +1,179 @@
+from os import environ
+
+import pytest
+from devtools import debug
+
+
+EMAIL_USER = "test@testmail.com"
+PWD_USER = "testpassword"
+
+
+async def test_register_as_user(invoke, register_user, caplog):
+    with pytest.raises(SystemExit):
+        await invoke(f"user register {EMAIL_USER} {PWD_USER}")
+    debug(caplog.text)
+    assert "403" in caplog.text
+    assert "Forbidden" in caplog.text
+
+
+@pytest.mark.parametrize("is_superuser", [True, False])
+async def test_register_as_superuser(invoke_as_superuser, is_superuser: bool):
+    if is_superuser:
+        res = await invoke_as_superuser(
+            f"user register {EMAIL_USER} {PWD_USER} --superuser"
+        )
+        debug(res.data)
+        assert res.retcode == 0
+        assert res.data["is_superuser"]
+    else:
+        res = await invoke_as_superuser(
+            f"user register {EMAIL_USER} {PWD_USER}"
+        )
+        debug(res.data)
+        assert res.retcode == 0
+        assert not res.data["is_superuser"]
+    assert res.data["email"] == EMAIL_USER
+
+
+async def test_list_as_user(invoke, register_user, caplog):
+    with pytest.raises(SystemExit):
+        await invoke("user list")
+    debug(caplog.text)
+    assert "403" in caplog.text
+    assert "Forbidden" in caplog.text
+
+
+async def test_list_as_superuser(invoke_as_superuser, register_user):
+    res = await invoke_as_superuser("user list")
+    debug(res.data)
+    assert res.retcode == 0
+    list_emails = [user["email"] for user in res.data]
+    debug(list_emails)
+    assert "admin@fractal.xy" in list_emails
+    assert environ["FRACTAL_USER"] in list_emails
+
+
+async def test_show_as_user(
+    invoke, invoke_as_superuser, register_user, caplog
+):
+    # Register a new user
+    res = await invoke_as_superuser(f"user register {EMAIL_USER} {PWD_USER}")
+    user_id = res.data["id"]
+    # Call fractal user show
+    with pytest.raises(SystemExit):
+        await invoke(f"user show {user_id}")
+    debug(caplog.text)
+    assert "403" in caplog.text
+    assert "Forbidden" in caplog.text
+
+
+async def test_show_as_superuser(invoke_as_superuser):
+    # Register a new user
+    res = await invoke_as_superuser(f"user register {EMAIL_USER} {PWD_USER}")
+    user_id = res.data["id"]
+    # Call fractal user show
+    await invoke_as_superuser(f"user show {user_id}")
+    debug(res.data)
+    assert res.retcode == 0
+    assert res.data["email"] == EMAIL_USER
+
+
+async def test_edit_as_user(
+    invoke, invoke_as_superuser, register_user, caplog
+):
+    # Register a new user
+    res = await invoke_as_superuser(f"user register {EMAIL_USER} {PWD_USER}")
+    user_id = res.data["id"]
+    # Call fractal user edit
+    with pytest.raises(SystemExit):
+        await invoke(f"user edit {user_id} --new-email email@something.xy")
+    debug(caplog.text)
+    assert "403" in caplog.text
+    assert "Forbidden" in caplog.text
+
+
+@pytest.mark.parametrize("new_is_superuser", [True, False])
+async def test_edit_as_superuser(invoke_as_superuser, new_is_superuser):
+    # Register a new user
+    res = await invoke_as_superuser(f"user register {EMAIL_USER} {PWD_USER}")
+    assert res.retcode == 0
+    user_id = res.data["id"]
+    # Call fractal user edit
+    NEW_EMAIL = "asd@asd.new"
+    NEW_SLURM_USER = "new_slurm"
+    cmd = (
+        f"user edit {user_id} "
+        f"--new-email {NEW_EMAIL} "
+        f"--new-slurm-user {NEW_SLURM_USER} "
+    )
+    if new_is_superuser:
+        cmd = f"{cmd} --make-superuser"
+    debug(cmd)
+    res = await invoke_as_superuser(cmd)
+    debug(res.data)
+    assert res.retcode == 0
+    assert res.data["email"] == NEW_EMAIL
+    assert res.data["slurm_user"] == NEW_SLURM_USER
+    assert res.data["is_superuser"] == new_is_superuser
+
+    # If the user was made a superuser, check that we can go back to normal
+    # user
+    if new_is_superuser:
+        cmd = f"user edit {user_id} --remove-superuser"
+        debug(cmd)
+        res = await invoke_as_superuser(cmd)
+        debug(res.data)
+        assert res.retcode == 0
+        assert not res.data["is_superuser"]
+
+
+async def test_edit_arguments(invoke_as_superuser):
+    # Test that superuser flags are mutually exclusive
+    with pytest.raises(SystemExit):
+        cmd = "user edit SOME_USER_ID --make-superuser --remove-superuser"
+        await invoke_as_superuser(cmd)
+
+
+async def test_delete_as_user(
+    invoke, invoke_as_superuser, register_user, caplog
+):
+    # Register a new user
+    res = await invoke_as_superuser(f"user register {EMAIL_USER} {PWD_USER}")
+    user_id = res.data["id"]
+    # Call fractal user edit
+    with pytest.raises(SystemExit):
+        await invoke(f"user delete {user_id}")
+    debug(caplog.text)
+    assert "403" in caplog.text
+    assert "Forbidden" in caplog.text
+
+
+async def test_delete_as_superuser(invoke_as_superuser, caplog):
+    # Register a new user
+    res = await invoke_as_superuser(f"user register {EMAIL_USER} {PWD_USER}")
+    user_id = res.data["id"]
+    # Call fractal user delete
+    res = await invoke_as_superuser(f"user delete {user_id}")
+    assert res.retcode == 0
+    # Check that user was not found
+    with pytest.raises(SystemExit):
+        res = await invoke_as_superuser(f"user show {user_id}")
+    debug(caplog.text)
+    assert "404" in caplog.text
+    assert "Not Found" in caplog.text
+
+
+async def test_whoami_as_user(invoke, register_user):
+    res = await invoke("user whoami")
+    assert res.retcode == 0
+    debug(res.data)
+    assert res.data["email"] == environ["FRACTAL_USER"]
+    assert not res.data["is_superuser"]
+
+
+async def test_whoami_as_superuser(invoke_as_superuser):
+    res = await invoke_as_superuser("user whoami")
+    assert res.retcode == 0
+    debug(res.data)
+    assert res.data["email"] == "admin@fractal.xy"
+    assert res.data["is_superuser"]