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

Discussion:
Turiok: Hi @allburov 

Sorry if I'm annoying.
Thanks for explaining all these points, I learn a lot of things related to Python and Pytest thanks to you.

Our goals are multiple but each time the idea is to put the information in an ELK stack and in HTML pages
allburov: @Turiok hi, no worries!
Sorry I'm so picky :D 
allburov: Good work!

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
+    )