Repository: flux-framework/flux-accounting
Pull request #597: cmd: add new `list-users` command
Author: login: cmoussa1
Labels: name: new feature
description: new feature
name: merge-when-passing
Created: 2025-03-19T20:27:39Z | Merged: 2025-03-21T03:19:25Z

#### Problem

As noted in #596, there is no way to list any of the rows in the `association_table`, similar to the `list-banks` command. It would also be nice if the table could be filtered by certain columns in the table, such as by queue, project, or limit.

---

This PR adds a new command to the flux-accounting command suite called `list-users`, which by default will list all of the rows in the `association_table` in a human-readable table format. An optional `--json` argument is also added to output the results in a JSON format:

```console
$ flux account list-users
username | bank | default_bank | shares
---------+------+--------------+-------
user1    | A    | A            | 1     
user2    | A    | A            | 1     
user2    | B    | A            | 1     
user3    | B    | B            | 100   
user4    | C    | C            | 100  
```

It accepts multiple optional arguments to filter the table by certain columns in the `association_table`.

```console
$ flux account list-users --shares=100
username | bank | default_bank | shares
---------+------+--------------+-------
user3    | B    | B            | 100   
user4    | C    | C            | 100
```

Its output can also be customized by column, e.g the person calling this command only wants to see the username of the association.

```console
$ flux account list-users --shares=100 --fields=username,bank
username | bank
---------+-----
user3    | B   
user4    | C   
```

Fixes #596 

Linked issue #596: feature request: `flux account list-users`
I've recently encountered a couple of use cases where the ability to list all users in the Flux accounting database would be useful. The specific use cases are finding all users who have access to a specific queue in order to clean up after DATs and finding all users with a specific default bank after adding new banks or before retiring old ones. I can also imagine wanting to get al users with a given node limit, etc.

So, the first level of this request would be something like a `flux account list-users` command that lists all users. The second level would be to add flags that would display a subset of users. E.g. `flux account list-users --default-bank=guests` would list all users whose default bank is `guests`.

I could also see this as being a modification to the `view-user` command rather than a new command. I.e. make the `USERNAME` argument to `view-user` optional so that `flux account view-user` with no other arguments would list all users and `flux account view-user --default-bank=guests` would list all users whose default bank is `guests`.

Diff:
diff --git a/src/bindings/python/fluxacct/accounting/user_subcommands.py b/src/bindings/python/fluxacct/accounting/user_subcommands.py
index 365f9194e..b958f723b 100755
--- a/src/bindings/python/fluxacct/accounting/user_subcommands.py
+++ b/src/bindings/python/fluxacct/accounting/user_subcommands.py
@@ -269,6 +269,61 @@ def view_user(conn, user, parsable=False, cols=None, list_banks=False):
         raise ValueError(f"view-user: {exc}")
 
 
+def list_users(conn, cols=None, json_fmt=False, **kwargs):
+    """
+    List all associations in the association_table in the flux-accounting DB. If
+    filters are passed in, limit the associations returned to the ones which fit
+    all filters.
+
+    Args:
+        conn: a SQLite connection object
+        cols: a list of columns from the table to include in the output. By default, all
+            columns are included.
+        filters: a list of optional constraints passed-in to filter the
+            association_table by.
+    """
+    # use all column names if none are passed in
+    cols = cols or fluxacct.accounting.ASSOCIATION_TABLE
+
+    # if any filters are passed in, make sure they are valid columns
+    table_filters = {key: val for key, val in kwargs.items() if val is not None}
+
+    try:
+        cur = conn.cursor()
+
+        sql.validate_columns(cols, fluxacct.accounting.ASSOCIATION_TABLE)
+        # construct SELECT statement
+        select_stmt = f"SELECT {', '.join(cols)} FROM association_table"
+        # filter by any constraints passed in
+        where_clauses = []
+        filters_list = []
+        for table_filter in table_filters:
+            if table_filter in ("queues", "projects"):
+                # we are filtering the table with a string; append wildcards ('%') to
+                # the string so we can match multiple cases (e.g the association belongs
+                # to more than one queue or project)
+                where_clauses.append(f"{table_filter} LIKE ?")
+                filters_list.append(f"%{table_filters[table_filter]}%")
+            else:
+                where_clauses.append(f"{table_filter} = ?")
+                filters_list.append(table_filters[f"{table_filter}"])
+
+        if where_clauses:
+            select_stmt += " WHERE " + " AND ".join(where_clauses)
+
+        cur.execute(select_stmt, tuple(filters_list))
+
+        # initialize AccountingFormatter object
+        formatter = fmt.AccountingFormatter(cur)
+        if json_fmt:
+            return formatter.as_json()
+        return formatter.as_table()
+    except sqlite3.Error as err:
+        raise sqlite3.Error(f"list-users: an sqlite3.Error occurred: {err}")
+    except ValueError as exc:
+        raise ValueError(f"list-users: {exc}")
+
+
 def add_user(
     conn,
     username,
diff --git a/src/cmd/flux-account-service.py b/src/cmd/flux-account-service.py
index 28a809676..42c5f5976 100755
--- a/src/cmd/flux-account-service.py
+++ b/src/cmd/flux-account-service.py
@@ -95,6 +95,7 @@ def __init__(self, flux_handle, conn):
             "view_project",
             "list_projects",
             "list_queues",
+            "list_users",
         ]
 
         privileged_endpoints = [
@@ -171,6 +172,34 @@ def view_user(self, handle, watcher, msg, arg):
                 msg, 0, f"a non-OSError exception was caught: {str(exc)}"
             )
 
+    # pylint: disable=no-self-use
+    def list_users(self, handle, watcher, msg, arg):
+        try:
+            val = u.list_users(
+                self.conn,
+                cols=msg.payload.get("fields").split(",")
+                if msg.payload.get("fields")
+                else None,
+                json_fmt=msg.payload.get("json"),
+                active=msg.payload.get("active"),
+                bank=msg.payload.get("bank"),
+                shares=msg.payload.get("shares"),
+                max_running_jobs=msg.payload.get("max_running_jobs"),
+                max_active_jobs=msg.payload.get("max_active_jobs"),
+                max_nodes=msg.payload.get("max_nodes"),
+                max_cores=msg.payload.get("max_cores"),
+                queues=msg.payload.get("queues"),
+                projects=msg.payload.get("projects"),
+            )
+
+            payload = {"list_users": val}
+
+            handle.respond(msg, payload)
+        except KeyError as exc:
+            handle.respond_error(msg, 0, f"missing key in payload: {exc}")
+        except Exception as exc:
+            handle.respond_error(msg, 0, str(exc))
+
     def add_user(self, handle, watcher, msg, arg):
         try:
             val = u.add_user(
diff --git a/src/cmd/flux-account.py b/src/cmd/flux-account.py
index 0c908a636..ad056a3ba 100755
--- a/src/cmd/flux-account.py
+++ b/src/cmd/flux-account.py
@@ -66,6 +66,64 @@ def add_view_user_arg(subparsers):
     )
 
 
+def add_list_users_arg(subparsers):
+    subparser_list_users = subparsers.add_parser(
+        "list-users",
+        help="list all associations in association_table",
+        formatter_class=flux.util.help_formatter(),
+    )
+    subparser_list_users.set_defaults(func="list_users")
+    subparser_list_users.add_argument(
+        "--fields",
+        type=str,
+        help="list of fields to include in output",
+        default=None,
+        metavar=f"{','.join(fluxacct.accounting.ASSOCIATION_TABLE)}",
+    )
+    subparser_list_users.add_argument(
+        "--json",
+        action="store_const",
+        const=True,
+        help="print output in JSON format",
+    )
+    subparser_list_users.add_argument(
+        "--active",
+        metavar="ACTIVE_STATUS",
+    )
+    subparser_list_users.add_argument(
+        "--bank",
+        metavar="BANK",
+    )
+    subparser_list_users.add_argument(
+        "--shares",
+        metavar="SHARES",
+    )
+    subparser_list_users.add_argument(
+        "--max-running-jobs",
+        metavar="MAX_RUNNING_JOBS",
+    )
+    subparser_list_users.add_argument(
+        "--max-active-jobs",
+        metavar="MAX_ACTIVE_JOBS",
+    )
+    subparser_list_users.add_argument(
+        "--max-nodes",
+        metavar="MAX_NODES",
+    )
+    subparser_list_users.add_argument(
+        "--max-cores",
+        metavar="MAX_CORES",
+    )
+    subparser_list_users.add_argument(
+        "--queues",
+        metavar="QUEUES",
+    )
+    subparser_list_users.add_argument(
+        "--projects",
+        metavar="PROJECTS",
+    )
+
+
 def add_add_user_arg(subparsers):
     subparser_add_user = subparsers.add_parser(
         "add-user",
@@ -742,6 +800,7 @@ def add_arguments_to_parser(parser, subparsers):
     add_path_arg(parser)
     add_output_file_arg(parser)
     add_view_user_arg(subparsers)
+    add_list_users_arg(subparsers)
     add_add_user_arg(subparsers)
     add_delete_user_arg(subparsers)
     add_edit_user_arg(subparsers)
@@ -786,6 +845,7 @@ def select_accounting_function(args, output_file, parser):
     # map each command to the corresponding accounting RPC call
     func_map = {
         "view_user": "accounting.view_user",
+        "list_users": "accounting.list_users",
         "add_user": "accounting.add_user",
         "delete_user": "accounting.delete_user",
         "edit_user": "accounting.edit_user",
diff --git a/t/Makefile.am b/t/Makefile.am
index ac3b1e481..a8b719226 100644
--- a/t/Makefile.am
+++ b/t/Makefile.am
@@ -50,6 +50,7 @@ TESTSCRIPTS = \
 	t1048-issue575.t \
 	t1049-issue580.t \
 	t1050-mf-priority-update-on-reload.t \
+	t1051-list-users.t \
 	t5000-valgrind.t \
 	python/t1000-example.py \
 	python/t1001_db.py \
diff --git a/t/t1051-list-users.t b/t/t1051-list-users.t
new file mode 100755
index 000000000..58d32e345
--- /dev/null
+++ b/t/t1051-list-users.t
@@ -0,0 +1,154 @@
+#!/bin/bash
+
+test_description='test list-users command'
+
+. `dirname $0`/sharness.sh
+FLUX_ACCOUNTING_DB=$(pwd)/FluxAccountingTest.db
+
+export TEST_UNDER_FLUX_NO_JOB_EXEC=y
+export TEST_UNDER_FLUX_SCHED_SIMPLE_MODE="limited=1"
+test_under_flux 1 job
+
+flux setattr log-stderr-level 1
+
+test_expect_success 'create flux-accounting DB' '
+	flux account -p ${FLUX_ACCOUNTING_DB} create-db
+'
+
+test_expect_success 'start flux-accounting service' '
+	flux account-service -p ${FLUX_ACCOUNTING_DB} -t
+'
+
+test_expect_success 'add some banks to the DB' '
+	flux account add-bank root 1 &&
+	flux account add-bank --parent-bank=root A 1 &&
+	flux account add-bank --parent-bank=root B 1 &&
+	flux account add-bank --parent-bank=root C 1
+'
+
+test_expect_success 'add some queues to the DB' '
+	flux account add-queue bronze &&
+	flux account add-queue silver &&
+	flux account add-queue gold
+'
+
+test_expect_success 'add some projects to the DB' '
+	flux account add-project leviathan_wakes &&
+	flux account add-project babylons_gate
+'
+
+test_expect_success 'add some users to the DB' '
+	flux account add-user --username=user1 --userid=5011 --bank=A &&
+	flux account add-user --username=user2 --userid=5012 --bank=A &&
+	flux account add-user --username=user2 --userid=5012 --bank=B &&
+	flux account add-user --username=user3 --userid=5013 --bank=B &&
+	flux account add-user --username=user4 --userid=5014 --bank=C
+'
+
+test_expect_success 'edit two associations to have different shares values than the rest' '
+	flux account edit-user user3 --shares=100 &&
+	flux account edit-user user4 --shares=100
+'
+
+test_expect_success 'edit associations to belong to different queues' '
+	flux account edit-user user1 --queues="bronze" &&
+	flux account edit-user user2 --queues="bronze,silver" &&
+	flux account edit-user user3 --queues="bronze,silver,gold" &&
+	flux account edit-user user4 --queues="bronze,silver"
+'
+
+test_expect_success 'edit associations to belong to different projects' '
+	flux account edit-user user1 --projects="leviathan_wakes" &&
+	flux account edit-user user2 --bank=B --projects="leviathan_wakes,babylons_gate"
+'
+
+test_expect_success 'call list-users --help' '
+	flux account list-users --help
+'
+
+test_expect_success 'pass in an invalid argument to list-users' '
+	test_must_fail flux account list-users --foo=bar > invalid_arg.out 2>&1 &&
+	grep "error: unrecognized arguments: --foo=bar" invalid_arg.out
+'
+
+# If no filters are passed, every association in the association_table will be
+# returned in the output.
+test_expect_success 'call list-users with no optional args to get all associations' '
+	flux account list-users --json >all_users.json &&
+	test_debug "jq -S . <all_users.json" &&
+	jq -e "length == 5" all_users.json 
+'
+
+# In the following set of tests, we pass certain filters to only get
+# the associations which fit the criteria of the filters passed in.
+test_expect_success 'filter associations by bank' '
+	flux account list-users --json --bank=A >bankA_users.json &&
+	test_debug "jq -S . <bankA_users.json" &&
+	jq -e "length == 2" bankA_users.json 
+'
+
+test_expect_success 'filter associations by shares' '
+	flux account list-users --json --shares=100 >100shares_users.json &&
+	test_debug "jq -S . <100shares_users.json" &&
+	jq -e "length == 2" 100shares_users.json 
+'
+
+test_expect_success 'filter associations by queues' '
+	flux account list-users --json --queues="bronze" >bronze_users.json &&
+	test_debug "jq -S . <bronze_users.json" &&
+	jq -e "length == 5" bronze_users.json
+'
+
+test_expect_success 'filter associations by more queues' '
+	flux account list-users --json --queues="gold" >gold_users.json &&
+	test_debug "jq -S . <gold_users.json" &&
+	jq -e "length == 1" gold_users.json
+'
+
+test_expect_success 'filter associations by project' '
+	flux account list-users --json --project="leviathan_wakes" >leviathan_wakes_users.json &&
+	test_debug "jq -S . <leviathan_wakes_users.json" &&
+	jq -e "length == 2" leviathan_wakes_users.json
+'
+
+test_expect_success 'delete an association and ensure we can filter by active status' '
+	flux account delete-user user4 C &&
+	flux account list-users --json --active=0 >inactive_users.json &&
+	test_debug "jq -S . <inactive_users.json" &&
+	jq -e "length == 1" inactive_users.json
+'
+
+# In the following test, we pass more than one field when listing the
+# associations to further filter the values returned.
+test_expect_success 'filter by more than one field' '
+	flux account list-users --json --bank=A --projects="leviathan_wakes" >multiple_filters_users.json &&
+	cat multiple_filters_users.json &&
+	test_debug "jq -S . <multiple_filters_users.json" &&
+	jq -e "length == 1" multiple_filters_users.json
+'
+
+# In the following set of tests, we customize the output to only include
+# certain columns in the output.
+test_expect_success 'customize JSON output of list-users' '
+	flux account list-users --json \
+		--bank=A --fields=username,bank,default_bank,shares > bankA_custom_output.json &&
+	grep "\"username\": \"user1\"" bankA_custom_output.json &&
+	grep "\"username\": \"user2\"" bankA_custom_output.json
+'
+
+test_expect_success 'customize table output of list-users' '
+	flux account list-users \
+		--bank=A --fields=username,bank,default_bank,shares > bankA_custom_table.out &&
+	grep "user1" bankA_custom_table.out &&
+	grep "user2" bankA_custom_table.out
+'
+
+test_expect_success 'remove flux-accounting DB' '
+	rm ${FLUX_ACCOUNTING_DB}
+'
+
+test_expect_success 'shut down flux-accounting service' '
+	flux python -c "import flux; flux.Flux().rpc(\"accounting.shutdown_service\").get()"
+'
+
+test_done


Reviews:
user: login: jameshcorbett
body: Looks good, some small comments.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T22:15:13Z
user: login: cmoussa1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-21T03:08:34Z

Review comments:
diff_hunk: @@ -171,6 +172,36 @@ def view_user(self, handle, watcher, msg, arg):
                 msg, 0, f"a non-OSError exception was caught: {str(exc)}"
             )
 
+    # pylint: disable=no-self-use
+    def list_users(self, handle, watcher, msg, arg):
+        try:
+            val = u.list_users(
+                self.conn,
+                cols=msg.payload.get("fields").split(",")
+                if msg.payload.get("fields")
+                else None,
+                json_fmt=msg.payload.get("json"),
+                active=msg.payload.get("active"),
+                bank=msg.payload.get("bank"),
+                shares=msg.payload.get("shares"),
+                max_running_jobs=msg.payload.get("max_running_jobs"),
+                max_active_jobs=msg.payload.get("max_active_jobs"),
+                max_nodes=msg.payload.get("max_nodes"),
+                max_cores=msg.payload.get("max_cores"),
+                queues=msg.payload.get("queues"),
+                projects=msg.payload.get("projects"),
+            )
+
+            payload = {"list_users": val}
+
+            handle.respond(msg, payload)
+        except KeyError as exc:
+            handle.respond_error(msg, 0, f"missing key in payload: {exc}")
+        except Exception as exc:
+            handle.respond_error(
+                msg, 0, f"a non-OSError exception was caught: {str(exc)}"
path: src/cmd/flux-account-service.py
user: login: jameshcorbett
body: why non-`OSError`? Couldn't the exception caught have been an `OSError`?
created_at: 2025-03-20T22:00:27Z
author_association: MEMBER
diff_hunk: @@ -269,6 +269,61 @@ def view_user(conn, user, parsable=False, cols=None, list_banks=False):
         raise ValueError(f"view-user: {exc}")
path: src/bindings/python/fluxacct/accounting/user_subcommands.py
user: login: jameshcorbett
body: commit message is out of date, default is no longer for JSON and there is no `--parsable` option?
created_at: 2025-03-20T22:01:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,150 @@
+#!/bin/bash
+
+test_description='test list-users command'
+
+. `dirname $0`/sharness.sh
+FLUX_ACCOUNTING_DB=$(pwd)/FluxAccountingTest.db
+
+export TEST_UNDER_FLUX_NO_JOB_EXEC=y
+export TEST_UNDER_FLUX_SCHED_SIMPLE_MODE="limited=1"
+test_under_flux 1 job
+
+flux setattr log-stderr-level 1
+
+test_expect_success 'create flux-accounting DB' '
+	flux account -p ${FLUX_ACCOUNTING_DB} create-db
+'
+
+test_expect_success 'start flux-accounting service' '
+	flux account-service -p ${FLUX_ACCOUNTING_DB} -t
+'
+
+test_expect_success 'add some banks to the DB' '
+	flux account add-bank root 1 &&
+	flux account add-bank --parent-bank=root A 1 &&
+	flux account add-bank --parent-bank=root B 1 &&
+	flux account add-bank --parent-bank=root C 1
+'
+
+test_expect_success 'add some queues to the DB' '
+	flux account add-queue bronze &&
+	flux account add-queue silver &&
+	flux account add-queue gold
+'
+
+test_expect_success 'add some projects to the DB' '
+	flux account add-project leviathan_wakes &&
+	flux account add-project babylons_gate
+'
+
+test_expect_success 'add some users to the DB' '
+	flux account add-user --username=user1 --userid=5011 --bank=A &&
+	flux account add-user --username=user2 --userid=5012 --bank=A &&
+	flux account add-user --username=user2 --userid=5012 --bank=B &&
+	flux account add-user --username=user3 --userid=5013 --bank=B &&
+	flux account add-user --username=user4 --userid=5014 --bank=C
+'
+
+test_expect_success 'edit two associations to have different shares values than the rest' '
+	flux account edit-user user3 --shares=100 &&
+	flux account edit-user user4 --shares=100
+'
+
+test_expect_success 'edit associations to belong to different queues' '
+	flux account edit-user user1 --queues="bronze" &&
+	flux account edit-user user2 --queues="bronze,silver" &&
+	flux account edit-user user3 --queues="bronze,silver,gold" &&
+	flux account edit-user user4 --queues="bronze,silver"
+'
+
+test_expect_success 'edit associations to belong to different projects' '
+	flux account edit-user user1 --projects="leviathan_wakes" &&
+	flux account edit-user user2 --bank=B --projects="leviathan_wakes,babylons_gate"
+'
+
+test_expect_success 'call list-users --help' '
+	flux account list-users --help
+'
+
+test_expect_success 'pass in an invalid argument to list-users' '
+	test_must_fail flux account list-users --foo=bar > invalid_arg.out 2>&1 &&
+	grep "error: unrecognized arguments: --foo=bar" invalid_arg.out
+'
+
+# If no filters are passed, every association in the association_table will be
+# returned in the output.
+test_expect_success 'call list-users with no optional args to get all associations' '
+	flux account list-users --json >all_users.json &&
+	test_debug "jq -S . <all_users.json" &&
+	jq -e "length == 5" all_users.json 
+'
+
+# In the following set of tests, we pass certain filters to only get
+# the associations which fit the criteria of the filters passed in.
+test_expect_success 'filter associations by bank' '
+	flux account list-users --json --bank=A >bankA_users.json &&
+	test_debug "jq -S . <bankA_users.json" &&
+	jq -e "length == 2" bankA_users.json 
+'
+
+test_expect_success 'filter associations by shares' '
+	flux account list-users --json --shares=100 >100shares_users.json &&
+	test_debug "jq -S . <100shares_users.json" &&
+	jq -e "length == 2" 100shares_users.json 
+'
+
+test_expect_success 'filter associations by queues' '
+	flux account list-users --json --queues="bronze" >bronze_users.json &&
+	test_debug "jq -S . <bronze_users.json" &&
+	jq -e "length == 5" bronze_users.json
+'
+
+test_expect_success 'filter associations by more queues' '
+	flux account list-users --json --queues="gold" >gold_users.json &&
+	test_debug "jq -S . <gold_users.json" &&
+	jq -e "length == 1" gold_users.json
+'
+
+test_expect_success 'filter associations by project' '
+	flux account list-users --json --project="leviathan_wakes" >leviathan_wakes_users.json &&
+	test_debug "jq -S . <leviathan_wakes_users.json" &&
+	jq -e "length == 2" leviathan_wakes_users.json
+'
+
+test_expect_success 'delete an association and ensure we can filter by active status' '
+	flux account delete-user user4 C &&
+	flux account list-users --json --active=0 >inactive_users.json &&
+	test_debug "jq -S . <inactive_users.json" &&
+	jq -e "length == 1" inactive_users.json
+'
+
+# In the following test, we pass more than one field when listing the
+# associations to further filter the values returned.
+test_expect_success 'filter by more than one field' '
+	flux account list-users --json --bank=A --projects="leviathan_wakes" >multiple_filters_users.json &&
+	cat multiple_filters_users.json &&
+	test_debug "jq -S . <multiple_filters_users.json" &&
+	jq -e "length == 1" multiple_filters_users.json
+'
+
+# In the following set of tests, we customize the output to only include
+# certain columns in the output.
+test_expect_success 'customize JSON output of list-users' '
+	flux account list-users --json \
+		--bank=A --fields=username,bank,default_bank,shares
path: t/t1051-list-users.t
user: login: jameshcorbett
body: Should there maybe be a check that this is outputting the expected results?
created_at: 2025-03-20T22:14:33Z
author_association: MEMBER
diff_hunk: @@ -171,6 +172,36 @@ def view_user(self, handle, watcher, msg, arg):
                 msg, 0, f"a non-OSError exception was caught: {str(exc)}"
             )
 
+    # pylint: disable=no-self-use
+    def list_users(self, handle, watcher, msg, arg):
+        try:
+            val = u.list_users(
+                self.conn,
+                cols=msg.payload.get("fields").split(",")
+                if msg.payload.get("fields")
+                else None,
+                json_fmt=msg.payload.get("json"),
+                active=msg.payload.get("active"),
+                bank=msg.payload.get("bank"),
+                shares=msg.payload.get("shares"),
+                max_running_jobs=msg.payload.get("max_running_jobs"),
+                max_active_jobs=msg.payload.get("max_active_jobs"),
+                max_nodes=msg.payload.get("max_nodes"),
+                max_cores=msg.payload.get("max_cores"),
+                queues=msg.payload.get("queues"),
+                projects=msg.payload.get("projects"),
+            )
+
+            payload = {"list_users": val}
+
+            handle.respond(msg, payload)
+        except KeyError as exc:
+            handle.respond_error(msg, 0, f"missing key in payload: {exc}")
+        except Exception as exc:
+            handle.respond_error(
+                msg, 0, f"a non-OSError exception was caught: {str(exc)}"
path: src/cmd/flux-account-service.py
user: login: cmoussa1
body: I think you're right. I can't seem to remember why I had specified that kind of exception message in the first place - it was a part of the first commit that added this service and I've copied the template to the other commands I've added. I should look into amending that exception message. For now, I've edited the `list-users` exception handling to just return the exception message. 
created_at: 2025-03-21T03:08:34Z
author_association: MEMBER

PR comments:
user: login: grondo
created_at: 2025-03-20T00:22:02Z
body: One quick comment is that the table output should probably be called "human readable" instead of "parseable", since it is the JSON output that is machine parsable. Unless I'm misunderstanding something. 

Also, for me I'd want a list users command that just dumps a list of users so it could be used in a shell loop, e.g.
```sh
for user in $(flux account list-users); do <something>; done
```
However, that's a minor point because you could presumably get that from the JSON output?
author_association: CONTRIBUTOR
user: login: cmoussa1
created_at: 2025-03-20T15:41:54Z
body: Oh that's a good thought! Maybe I'll rename the table output to `--table` or something?

I can also change the default behavior to output _just_ usernames and keep the ability to customize any additional columns as an optional argument? What do you think?
author_association: MEMBER
user: login: grondo
created_at: 2025-03-20T16:09:22Z
body: We'll probably want @ryanday36's feedback, but what I'd expect from a `list-users` command is a list of all unique users by default, with the filtering options as Ryan suggested. I do like the ability to dump the info from the db by user as a table (for human consumption) or JSON (for use with tools like `jq`), so maybe this could be another subcommand (if there isn't one already) or the `--table` and `--json` options like you suggest.

One other suggestion is that it might be good to standardize the behavior of the `flux account` subcommands in this regard. For example, maybe all `list-*` commands just output a straight list of items, with `--table` and `--json` options to get extended output in table and json forms. 

Just a suggestion at this point though! Definitely up to you and @ryanday36!
author_association: CONTRIBUTOR
user: login: cmoussa1
created_at: 2025-03-20T16:18:35Z
body: I really like that suggestion! Getting @ryanday36's feedback would be helpful here.

Totally agree with you that standardizing the `list-*` commands would be a helpful improvement. This current implementation currently lines up with the `list-banks` command, but if we want to go in this direction, I could post a PR later to standardize the behavior for both. 
author_association: MEMBER
user: login: ryanday36
created_at: 2025-03-20T18:53:34Z
body: First off, this looks really good. I think that having a human-readable table output as the default and the option to output JSON for parsing in scripts would fit how I expect to use this best. If I'm just doing something quick and dirty I'd like to be able to visually inspect the output without adding flags or piping into `jq`, whereas if I'm consuming the output in a script I don't mind typing out extra flags to get exactly what I want. I'd also lean toward displaying all of the columns by default since I can get just the users (or other individual columns) pretty easily with that `--fields` flag.

For the future PR standardizing output behavior, it might be good to also think about making that `--fields` flag act like the output strings from `flux-core` commands (and maybe call it `-o/--format` too). I often find myself doing things like `<some Slurm command> | awk '{print $1}'` because the Slurm DB commands use different keywords and formats to specify their output than the commands that query the slurmctld and just piping into awk is an easier mental shortcut for me than remembering how a particular command needs its output flags formatted.
author_association: CONTRIBUTOR
user: login: cmoussa1
created_at: 2025-03-20T20:12:46Z
body: @ryanday36 this is super helpful, thanks. OK, why don't I go ahead and adjust this PR to the following:

* Make the default output a human-readable table with JSON output an optional argument. I'll keep the behavior to output all columns by default and keep the `--fields` flag as is.
* I'll open up an issue to track the standardization of the `list-*` (and perhaps the `view-*`?) commands. There might be some helpful discussion/ideas when thinking about using `-o/--format` for the flux-accounting commands (I already have this implemented for the `view-job-records` command).
author_association: MEMBER
user: login: cmoussa1
created_at: 2025-03-20T20:50:51Z
body: Just force-pushed those fixes mentioned above.
author_association: MEMBER