Repository: NatLibFi/Annif (language: python)

Issue:
Improve outputs of project inspection CLI commands
- Makes the column widths of the output of `annif list-projects` and `annif list-vocabs` dynamic to not spend unnecessary space in terminal. For vocabularies the benefit of this is not so great, but implementing this for both listing commands  keeps the functionalities aligned. The separator for columns is 2 spaces.

- Adds vocabulary ID and modification time columns to the output of `list-projects` implementing issue #686.

- Converts the project modification timestamps to current timezone (from UTC) and formats them to YYYY-MM-DD hh:mm:ss in the outputs of `list-projects` and `show-project`, using `-` in place of `None`.

- Adds "Backend" line the output of `show-project`


An example output of `list-projects`:
```
Project ID                Project Name                   Vocabulary ID  Language  Trained  Modification time  
--------------------------------------------------------------------------------------------------------------
tfidf-fi                  TF-IDF Finnish                 yso            fi        True     2023-04-21 10:28:37
tfidf-sv                  TF-IDF Swedish                 yso            sv        True     2023-04-21 10:30:17
tfidf-en                  TF-IDF English                 yso            en        True     2023-04-21 10:30:45
fasttext-fi               fastText Finnish               yso            fi        True     2023-04-21 10:33:16
fasttext-sv               fastText Swedish               yso            sv        True     2023-04-21 10:32:25
fasttext-en               fastText English               yso            en        True     2023-04-21 10:31:28
omikuji-parabel-fi        Omikuji Parabel Finnish        yso            fi        True     2023-04-21 10:35:14
omikuji-parabel-sv        Omikuji Parabel Swedish        yso            sv        True     2023-04-21 10:32:12
omikuji-parabel-en        Omikuji Parabel English        yso            en        True     2023-04-21 10:31:47
mllm-fi                   YSO MLLM Finnish               yso            fi        False    -                  
mllm-en                   YSO MLLM English               yso            en        False    -                  
mllm-sv                   YSO MLLM Swedish               yso            sv        False    -                  
yake-fi                   YAKE Finnish                   yso            fi        True     2023-04-21 10:23:56
ensemble-fi               Ensemble Finnish               yso            fi        False    2023-04-21 10:33:16
ensemble-sv               Ensemble Swedish               yso            sv        False    2023-04-21 10:32:25
ensemble-en               Ensemble English               yso            en        False    2023-04-21 10:31:28
ykl-omikuji-parabel-fi    YKL Omikuji Parabel Finnish    ykl            fi        False    -                  
thema-omikuji-parabel-fi  Thema Omikuji Parabel Finnish  thema-fi       fi        False    -                  
```

An example output of `list-vocabs`:
```
Vocabulary ID  Languages    Size   Loaded
-----------------------------------------
yso            en,fi,se,sv  38179  True  
ykl            -            -      False 
thema-fi       -            -      False 
```

An example output of `show-project`:
```
Project ID:        fasttext-fi
Project Name:      fastText Finnish
Language:          fi
Vocabulary:        yso
Vocab language:    fi
Access:            public
Backend:           fasttext
Trained:           True
Modification time: 2023-04-21 10:33:16
```


Discussion / hints:
## [Codecov](https://codecov.io/gh/NatLibFi/Annif/pull/694?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi) Report
Patch coverage: **`100.00`**% and no project coverage change.
> Comparison is base [(`3451bd3`)](https://codecov.io/gh/NatLibFi/Annif/commit/3451bd351273e3425a2bea683f8ac2bf3618278b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi) 99.63% compared to head [(`0cc9fe3`)](https://codecov.io/gh/NatLibFi/Annif/pull/694?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi) 99.63%.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main     #694   +/-   ##
=======================================
  Coverage   99.63%   99.63%           
=======================================
  Files          89       89           
  Lines        6222     6253   +31     
=======================================
+ Hits         6199     6230   +31     
  Misses         23       23           
```


| [Impacted Files](https://codecov.io/gh/NatLibFi/Annif/pull/694?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi) | Coverage Δ | |
|---|---|---|
| [annif/cli.py](https://codecov.io/gh/NatLibFi/Annif/pull/694?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi#diff-YW5uaWYvY2xpLnB5) | `99.65% <100.00%> (+<0.01%)` | :arrow_up: |
| [annif/cli\_util.py](https://codecov.io/gh/NatLibFi/Annif/pull/694?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi#diff-YW5uaWYvY2xpX3V0aWwucHk=) | `98.98% <100.00%> (+0.11%)` | :arrow_up: |
| [tests/test\_cli.py](https://codecov.io/gh/NatLibFi/Annif/pull/694?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi#diff-dGVzdHMvdGVzdF9jbGkucHk=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi)

</details>

[:umbrella: View full report in Codecov by Sentry](https://codecov.io/gh/NatLibFi/Annif/pull/694?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=NatLibFi).

I started to think adding also "Backend" column to the output of `list-projects`, but maybe that is too much detail in there. But backend could be shown by `show-projects`. 
Yet another addition would be to dump project configuration to output of `show-project`.

Other possible items to be added to the `show-project` output have been listed in the old issue https://github.com/NatLibFi/Annif/issues/329.
> Yet another addition would be to dump project configuration to output of `show-project`.

The output for project configuration could be like this (hiding the parameters that are already shown by the above lines):
```
$ annif show-project fasttext-fi 
Project ID:        fasttext-fi
Project Name:      fastText Finnish
Language:          fi
Vocabulary:        yso
Vocab language:    fi
Access:            public
Trained:           True
Modification time: 2023-04-21 10:33:16
Parameters:
  backend:         fasttext
  analyzer:        voikko(fi)
  dim:             500
  lr:              0.25
  epoch:           30
  loss:            hs
  limit:           100
  chunksize:       24
```
Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=NatLibFi_Annif&pullRequest=694)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=NatLibFi_Annif&pullRequest=694&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=NatLibFi_Annif&pullRequest=694) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=NatLibFi_Annif&pullRequest=694&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=NatLibFi_Annif&pullRequest=694&metric=new_duplicated_lines_density&view=list)


Maybe simplest would be to merge this as it is now, then possibly work on the project configuration display in a follow-up PR?

Fix patch:
diff --git a/annif/hfh_util.py b/annif/hfh_util.py
index 7249bd82..09cbb445 100644
--- a/annif/hfh_util.py
+++ b/annif/hfh_util.py
@@ -17,6 +17,7 @@
 from flask import current_app
 
 import annif
+from annif import cli_util
 from annif.config import AnnifConfigCFG
 from annif.exception import OperationFailedException
 from annif.project import Access, AnnifProject
@@ -347,18 +348,27 @@ def _update_projects_section(text, configs):
 
 
 def _create_projects_section(configs):
-    content = f"{AUTOUPDATING_START}\n## Projects\n"
-
-    template = "{0:<19} {1:<23} {2:<15} {3:<8}\n"
-    header = template.format("Project ID", "Project Name", "Vocabulary ID", "Language")
-    content += "```\n" + header + "-" * len(header.strip()) + "\n"
-
-    for proj_id in configs.project_ids:
-        project = configs[proj_id]
-        content += template.format(
+    column_headings = (
+        "Project ID",
+        "Project Name",
+        "Vocabulary ID",
+        "Language",
+    )
+    table = [
+        (
             proj_id,
-            project["name"],
-            project["vocab"],
-            project["language"],
+            configs[proj_id]["name"],
+            configs[proj_id]["vocab"],
+            configs[proj_id]["language"],
         )
+        for proj_id in configs.project_ids
+    ]
+    template = cli_util.make_list_template(column_headings, *table) + "\n"
+
+    header = template.format(*column_headings)
+
+    content = f"{AUTOUPDATING_START}\n## Projects\n"
+    content += "```\n" + header + "-" * len(header.strip()) + "\n"
+    for row in table:
+        content += template.format(*row)
     return content + "```\n" + AUTOUPDATING_END


Test patch:
diff --git a/tests/test_hfh_util.py b/tests/test_hfh_util.py
index 0bc22021..7b59ad4e 100644
--- a/tests/test_hfh_util.py
+++ b/tests/test_hfh_util.py
@@ -204,9 +204,8 @@ def test_upsert_modelcard_update_existing(read_text, glob, load, push_to_hub, pr
     load.assert_called_once_with(repo_id)
 
     card = load.return_value
-    retained_project_list_content = (
-        "dummy-en            Dummy English           dummy           en"
-    )
+    retained_project_list_content = "dummy-en    Dummy English  dummy          en      "
+
     assert retained_project_list_content in card.text
     assert sorted(card.data.language) == ["en", "fi"]
     card.push_to_hub.assert_called_once_with(
@@ -227,8 +226,8 @@ def test_update_modelcard_projects_section_append_new():
 <!--- start-of-autoupdating-part --->
 ## Projects
 ```
-Project ID          Project Name            Vocabulary ID   Language
---------------------------------------------------------------------
+Project ID  Project Name  Vocabulary ID  Language
+-------------------------------------------------
 ```
 <!--- end-of-autoupdating-part --->"""
 
@@ -250,8 +249,8 @@ def test_update_modelcard_projects_section_update_existing():
 <!--- start-of-autoupdating-part --->
 ## Projects
 ```
-Project ID          Project Name            Vocabulary ID   Language
---------------------------------------------------------------------
+Project ID  Project Name  Vocabulary ID  Language
+-------------------------------------------------
 ```
 <!--- end-of-autoupdating-part --->\n"""
 
@@ -266,9 +265,9 @@ def test_update_modelcard_projects_section_update_existing():
 <!--- start-of-autoupdating-part --->
 ## Projects
 ```
-Project ID          Project Name            Vocabulary ID   Language
---------------------------------------------------------------------
-dummy-fi            Dummy Finnish           dummy           fi      \n```
+Project ID  Project Name   Vocabulary ID  Language
+--------------------------------------------------
+dummy-fi    Dummy Finnish  dummy          fi      \n```
 <!--- end-of-autoupdating-part --->
 """