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

Issue:
Command `fractal -u USER -p PWD` should not require env variables `FRACTAL_USER` and `FRACTAL_PASSWORD` to be set
The  check is currently done upon loading configs, and it has a block like:
```python
    FRACTAL_USER: str = fail_getenv("FRACTAL_USER")
    FRACTAL_PASSWORD: str = fail_getenv("FRACTAL_PASSWORD")
    SLURM_USER: Optional[str] = getenv("SLURM_USER")
```

The `fail_getenv` should become `getenv`, and the check that they are not `None` should move to client.py (roughly [here](https://github.com/fractal-analytics-platform/fractal/blob/main/fractal/client.py#L55-L57)).

In the current state, you always have to declare those two env variables, even when they are going to be overridden by the `-u/-p` arguments.
Command `fractal -u USER -p PWD` should not require env variables `FRACTAL_USER` and `FRACTAL_PASSWORD` to be set
The  check is currently done upon loading configs, and it has a block like:
```python
    FRACTAL_USER: str = fail_getenv("FRACTAL_USER")
    FRACTAL_PASSWORD: str = fail_getenv("FRACTAL_PASSWORD")
    SLURM_USER: Optional[str] = getenv("SLURM_USER")
```

The `fail_getenv` should become `getenv`, and the check that they are not `None` should move to client.py (roughly [here](https://github.com/fractal-analytics-platform/fractal/blob/main/fractal/client.py#L55-L57)).

In the current state, you always have to declare those two env variables, even when they are going to be overridden by the `-u/-p` arguments.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..6de753bca
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# 1.0.1
+
+* Make `FRACTAL_USER/FRACTAL_PASSWORD` env variables optional (\#416).
+
+# 1.0.0
+
+* First version in this CHANGELOG.
diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
new file mode 100644
index 000000000..d805fdc3f
--- /dev/null
+++ b/docs/source/changelog.rst
@@ -0,0 +1,8 @@
+Changelog
+=========
+
+.. note::
+   Numbers like (#123) point to `closed Pull Requests on the fractal-tasks-core repository <https://github.com/fractal-analytics-platform/fractal/pulls?q=is%3Apr+is%3Aclosed>`_.
+
+.. include:: ../../CHANGELOG.md
+  :parser: myst_parser.sphinx_
diff --git a/docs/source/index.rst b/docs/source/index.rst
index d49614b94..d8b7a1fe8 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -21,3 +21,4 @@ This documentation concerns the **Fractal Client**. Find more information about
    Installation and usage <install>
    Command-line interface <cli>
    How to contribute <contribute>
+   Changelog <changelog>
diff --git a/fractal/client.py b/fractal/client.py
index 50e747986..07c255865 100644
--- a/fractal/client.py
+++ b/fractal/client.py
@@ -16,6 +16,8 @@
 import logging
 from sys import argv
 from typing import List
+from typing import Tuple
+from typing import Union
 
 from httpx import AsyncClient
 from httpx import ConnectError
@@ -28,6 +30,44 @@
 from .parser import parser_main
 
 
+class MissingCredentialsError(RuntimeError):
+    pass
+
+
+def _check_credentials(
+    *, username: Union[str, None], password: Union[str, None]
+) -> Tuple[str, str]:
+    """
+    Check that username and password are defined
+
+    Arguments:
+        username: Username
+        password: Password
+
+    Raises:
+        MissingCredentialsError: If either `username` of `password` is `None`.
+    """
+    if not username:
+        message = (
+            "FRACTAL_USER variable not defined."
+            "\nPossible options: \n"
+            + "    1. Set --user argument;\n"
+            + "    2. Define FRACTAL_USER in a .fractal.env file;\n"
+            + "    3. Define FRACTAL_USER as an environment variable."
+        )
+        raise MissingCredentialsError(message)
+    if not password:
+        message = (
+            "FRACTAL_PASSWORD variable not defined."
+            "\nPossible options: \n"
+            + "    1. Set --password argument;\n"
+            + "    2. Define FRACTAL_PASSWORD in a .fractal.env file;\n"
+            + "    3. Define FRACTAL_PASSWORD as an environment variable."
+        )
+        raise MissingCredentialsError(message)
+    return (username, password)
+
+
 async def handle(cli_args: List[str] = argv):
     args = parser_main.parse_args(cli_args[1:])
 
@@ -55,6 +95,9 @@ async def handle(cli_args: List[str] = argv):
             # 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
+            username, password = _check_credentials(
+                username=username, password=password
+            )
             async with AuthClient(
                 username=username, password=password
             ) as client:
diff --git a/fractal/config.py b/fractal/config.py
index 605150c9e..3383b1ee1 100644
--- a/fractal/config.py
+++ b/fractal/config.py
@@ -37,8 +37,8 @@ class Settings(BaseSettings):
 
     FRACTAL_LOGGING_LEVEL: int = logging.INFO
 
-    FRACTAL_USER: str = fail_getenv("FRACTAL_USER")
-    FRACTAL_PASSWORD: str = fail_getenv("FRACTAL_PASSWORD")
+    FRACTAL_USER: Optional[str] = getenv("FRACTAL_USER")
+    FRACTAL_PASSWORD: Optional[str] = getenv("FRACTAL_PASSWORD")
     SLURM_USER: Optional[str] = getenv("SLURM_USER")
 
     FRACTAL_SERVER: str = getenv("FRACTAL_SERVER", "http://localhost:8000")
diff --git a/poetry.lock b/poetry.lock
index 19d39391d..1a377dc26 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1339,6 +1339,22 @@ pytest = ">=6.1.0"
 [package.extras]
 testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
 
+[[package]]
+name = "pytest-pretty"
+version = "1.0.1"
+description = "pytest plugin for printing summary data as I want it"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pytest_pretty-1.0.1-py3-none-any.whl", hash = "sha256:c1039f05ee1a11e1d9e586faea79c7437d74d899d95e8f2824e884d2117329bd"},
+    {file = "pytest_pretty-1.0.1.tar.gz", hash = "sha256:d5221cb69384d581b490edd15e22d607e00772e02e6a393292626c626ff1c075"},
+]
+
+[package.dependencies]
+pytest = ">=7"
+rich = ">=12"
+
 [[package]]
 name = "python-dotenv"
 version = "0.20.0"
@@ -1952,4 +1968,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "ed1ae51804f625dbcb89ec56fef61c1bd6462db48d58e6edd1ecf50787c40791"
+content-hash = "88df7223e1f18430bc0040f6837cdbc8048d85aa5d94b68c52e71f82b188ccb5"
diff --git a/pyproject.toml b/pyproject.toml
index 78373731c..4dc0f486d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ bumpver = "^2022.1118"
 pytest-asyncio = "^0.19.0"
 fractal-server = "^1.0.1"
 coverage = {extras = ["toml"], version = "^6.5.0"}
+pytest-pretty = "^1.0.1"
 
 [tool.poetry.group.docs.dependencies]
 sphinx = "^5.3.0"


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 448e30f05..16d1bcca3 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,9 +1,12 @@
+import shlex
 from os import environ
 
 import httpx
+import pytest
 from devtools import debug
 
 from fractal import __VERSION__
+from fractal.client import MissingCredentialsError
 
 
 DEFAULT_TEST_EMAIL = environ["FRACTAL_USER"]
@@ -57,3 +60,28 @@ async def test_bad_credentials(invoke):
     res.show()
     assert res.retcode != 0
     assert "BAD_CREDENTIALS" in res.data
+
+
+async def test_missing_credentials(monkeypatch):
+    """
+    GIVEN an invocation with missing credentials
+    THEN the client raises a MissingCredentialsError
+    """
+
+    # Define patched settings
+    from fractal.config import Settings
+
+    patched_settings = Settings()
+    patched_settings.FRACTAL_USER = None
+
+    with monkeypatch.context() as m:
+        import fractal
+        from fractal.client import handle
+
+        m.setattr(fractal.client, "settings", patched_settings)
+        debug(fractal.config.settings)
+        with pytest.raises(MissingCredentialsError) as e:
+            await handle(shlex.split("fractal user whoami"))
+        debug(e.value)
+        debug(e.value.args[0])
+        assert "FRACTAL_USER" in e.value.args[0]