Repository: devopshq/artifactory-cleanup

Issue:
Output format to Json
We would like a feature request to have the output table to Json format.

The PrettyTable libraries can do it : https://pypi.org/project/prettytable/ 
If I have time, I would propose a PR

Comments:

Patch:
diff --git a/README.md b/README.md
index c0a63ec..ee5afde 100644
--- a/README.md
+++ b/README.md
@@ -135,6 +135,12 @@ artifactory-cleanup --days-in-future=10
 # Not satisfied with built-in rules? Write your own rules in python and connect them!
 artifactory-cleanup --load-rules=myrule.py
 docker run -v "$(pwd)":/app devopshq/artifactory-cleanup artifactory-cleanup --load-rules=myrule.py
+
+# Save the table summary in a file
+artifactory-cleanup --output=myfile.txt
+
+# Save the summary in a Json file
+artifactory-cleanup --output=myfile.txt --output-format=json
 ```
 
 # Rules
diff --git a/artifactory_cleanup/cli.py b/artifactory_cleanup/cli.py
index 312c999..b168ecf 100644
--- a/artifactory_cleanup/cli.py
+++ b/artifactory_cleanup/cli.py
@@ -1,3 +1,4 @@
+import json
 import logging
 import sys
 from datetime import timedelta, date
@@ -5,6 +6,7 @@ from datetime import timedelta, date
 import requests
 from hurry.filesize import size
 from plumbum import cli
+from plumbum.cli.switches import Set
 from prettytable import PrettyTable
 from requests.auth import HTTPBasicAuth
 
@@ -62,6 +64,19 @@ class ArtifactoryCleanupCLI(cli.Application):
         mandatory=False,
     )
 
+    _output_file = cli.SwitchAttr(
+        "--output", help="Choose the output file", mandatory=False
+    )
+
+    _output_format = cli.SwitchAttr(
+        "--output-format",
+        Set("table", "json", case_sensitive=False),
+        help="Choose the output format",
+        default="table",
+        requires=["--output"],
+        mandatory=False,
+    )
+
     @property
     def VERSION(self):
         # To prevent circular imports
@@ -83,6 +98,36 @@ class ArtifactoryCleanupCLI(cli.Application):
             print(f"Simulating cleanup actions that will occur on {today}")
         return today
 
+    def _format_table(self, result) -> PrettyTable:
+        table = PrettyTable()
+        table.field_names = ["Cleanup Policy", "Files count", "Size"]
+        table.align["Cleanup Policy"] = "l"
+
+        for policy_result in result["policies"]:
+            row = [
+                policy_result["name"],
+                policy_result["file_count"],
+                size(policy_result["size"]),
+            ]
+            table.add_row(row)
+
+        table.add_row(["", "", ""])
+        table.add_row(["Total size: {}".format(size(result["total_size"])), "", ""])
+        return table
+
+    def _print_table(self, result: dict):
+        print(self._format_table(result))
+
+    def _create_output_file(self, result, filename, format):
+        text = None
+        if format == "table":
+            text = self._format_table(result).get_string()
+        else:
+            text = json.dumps(result)
+
+        with open(filename, "w") as file:
+            file.write(text)
+
     def main(self):
         today = self._get_today()
         if self._load_rules:
@@ -112,9 +157,7 @@ class ArtifactoryCleanupCLI(cli.Application):
         if self._policy:
             cleanup.only(self._policy)
 
-        table = PrettyTable()
-        table.field_names = ["Cleanup Policy", "Files count", "Size"]
-        table.align["Cleanup Policy"] = "l"
+        result = {"policies": [], "total_size": 0}
         total_size = 0
 
         block_ctx_mgr, test_ctx_mgr = get_context_managers()
@@ -124,16 +167,21 @@ class ArtifactoryCleanupCLI(cli.Application):
             if summary is None:
                 continue
             total_size += summary.artifacts_size
-            row = [
-                summary.policy_name,
-                summary.artifacts_removed,
-                size(summary.artifacts_size),
-            ]
-            table.add_row(row)
 
-        table.add_row(["", "", ""])
-        table.add_row(["Total size: {}".format(size(total_size)), "", ""])
-        print(table)
+            result["policies"].append(
+                {
+                    "name": summary.policy_name,
+                    "file_count": summary.artifacts_removed,
+                    "size": summary.artifacts_size,
+                }
+            )
+
+        result["total_size"] = total_size
+
+        self._print_table(result)
+
+        if self._output_file:
+            self._create_output_file(result, self._output_file, self._output_format)
 
 
 if __name__ == "__main__":


Test patch:
diff --git a/tests/data/expected_output.txt b/tests/data/expected_output.txt
new file mode 100644
index 0000000..2471489
--- /dev/null
+++ b/tests/data/expected_output.txt
@@ -0,0 +1,8 @@
++--------------------------------------------------------+-------------+------+
+| Cleanup Policy                                         | Files count | Size |
++--------------------------------------------------------+-------------+------+
+| Remove all files from repo-name-here older then 7 days |      1      | 528B |
+| Use your own rules!                                    |      1      | 528B |
+|                                                        |             |      |
+| Total size: 1K                                         |             |      |
++--------------------------------------------------------+-------------+------+
\ No newline at end of file
diff --git a/tests/test_cli.py b/tests/test_cli.py
index f5c5afe..2869e23 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1,4 +1,6 @@
+import json
 import pytest
+from filecmp import cmp
 
 from artifactory_cleanup import ArtifactoryCleanupCLI
 
@@ -69,3 +71,101 @@ def test_destroy(capsys, shared_datadir, requests_mock):
         last_request.url
         == "https://repo.example.com/artifactory/repo-name-here/path/to/file/filename1.json"
     )
+
+
+@pytest.mark.usefixtures("requests_repo_name_here")
+def test_output_table(capsys, shared_datadir, requests_mock):
+    _, code = ArtifactoryCleanupCLI.run(
+        [
+            "ArtifactoryCleanupCLI",
+            "--config",
+            str(shared_datadir / "cleanup.yaml"),
+            "--load-rules",
+            str(shared_datadir / "myrule.py"),
+        ],
+        exit=False,
+    )
+    stdout, stderr = capsys.readouterr()
+    print(stdout)
+    assert code == 0, stdout
+    assert (
+        "| Cleanup Policy                                         | Files count | Size |"
+        in stdout
+    )
+
+
+@pytest.mark.usefixtures("requests_repo_name_here")
+def test_output_table(capsys, shared_datadir, requests_mock, tmp_path):
+    output_file = tmp_path / "output.txt"
+    _, code = ArtifactoryCleanupCLI.run(
+        [
+            "ArtifactoryCleanupCLI",
+            "--config",
+            str(shared_datadir / "cleanup.yaml"),
+            "--load-rules",
+            str(shared_datadir / "myrule.py"),
+            "--output-format",
+            "table",
+            "--output",
+            str(output_file),
+        ],
+        exit=False,
+    )
+    stdout, stderr = capsys.readouterr()
+    print(stdout)
+    assert code == 0, stdout
+    assert cmp(output_file, shared_datadir / "expected_output.txt") is True
+
+
+@pytest.mark.usefixtures("requests_repo_name_here")
+def test_output_json(capsys, shared_datadir, requests_mock, tmp_path):
+    output_json = tmp_path / "output.json"
+    _, code = ArtifactoryCleanupCLI.run(
+        [
+            "ArtifactoryCleanupCLI",
+            "--config",
+            str(shared_datadir / "cleanup.yaml"),
+            "--load-rules",
+            str(shared_datadir / "myrule.py"),
+            "--output-format",
+            "json",
+            "--output",
+            str(output_json),
+        ],
+        exit=False,
+    )
+    stdout, stderr = capsys.readouterr()
+    assert code == 0, stdout
+    with open(output_json, "r") as file:
+        assert json.load(file) == {
+            "policies": [
+                {
+                    "name": "Remove all files from repo-name-here older then 7 days",
+                    "file_count": 1,
+                    "size": 528,
+                },
+                {"name": "Use your own rules!", "file_count": 1, "size": 528},
+            ],
+            "total_size": 1056,
+        }
+
+
+@pytest.mark.usefixtures("requests_repo_name_here")
+def test_require_output_json(capsys, shared_datadir, requests_mock):
+    _, code = ArtifactoryCleanupCLI.run(
+        [
+            "ArtifactoryCleanupCLI",
+            "--config",
+            str(shared_datadir / "cleanup.yaml"),
+            "--load-rules",
+            str(shared_datadir / "myrule.py"),
+            "--output-format",
+            "json",
+        ],
+        exit=False,
+    )
+    assert code == 2, stdout
+    stdout, stderr = capsys.readouterr()
+    assert (
+        "Error: Given --output-format, the following are missing ['output']" in stdout
+    )