summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-06-30 10:58:02 +0200
committerDanilo M. <danix@danix.xyz>2026-06-30 10:58:02 +0200
commit09dddee3ebbbbbb8ed6eac74370232e76e84d7bb (patch)
tree869a37bce74d61b9ae557c8c9366bb70b8f85dc7
parent092e05361a534a8f35d7b551afa77004e30cf201 (diff)
downloadfirefly-cli-09dddee3ebbbbbb8ed6eac74370232e76e84d7bb.tar.gz
firefly-cli-09dddee3ebbbbbb8ed6eac74370232e76e84d7bb.zip
feat: output emit and envelope unwrap
-rw-r--r--firefly_cli/output.py37
-rw-r--r--tests/unit/test_output.py31
2 files changed, 68 insertions, 0 deletions
diff --git a/firefly_cli/output.py b/firefly_cli/output.py
new file mode 100644
index 0000000..8da2b1f
--- /dev/null
+++ b/firefly_cli/output.py
@@ -0,0 +1,37 @@
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only
+import json
+import sys
+
+def unwrap(resp):
+ """Flatten Firefly's JSON:API envelope to plain id+attributes objects."""
+ def flat(item):
+ out = {"id": item.get("id")}
+ out.update(item.get("attributes", {}))
+ return out
+ data = resp.get("data", resp)
+ if isinstance(data, list):
+ return [flat(i) for i in data]
+ if isinstance(data, dict) and "attributes" in data:
+ return flat(data)
+ return data
+
+def emit(data, human=False, stream=None):
+ stream = stream or sys.stdout
+ if not human:
+ json.dump(data, stream, indent=2, default=str)
+ stream.write("\n")
+ return
+ rows = data if isinstance(data, list) else [data]
+ if not rows:
+ stream.write("(no results)\n")
+ return
+ cols = list(rows[0].keys())
+ widths = {c: max(len(c), *(len(str(r.get(c, ""))) for r in rows)) for c in cols}
+ stream.write(" ".join(c.ljust(widths[c]) for c in cols) + "\n")
+ for r in rows:
+ stream.write(" ".join(str(r.get(c, "")).ljust(widths[c]) for c in cols) + "\n")
+
+def emit_error(message, stream=None):
+ stream = stream or sys.stderr
+ json.dump({"error": message}, stream)
+ stream.write("\n")
diff --git a/tests/unit/test_output.py b/tests/unit/test_output.py
new file mode 100644
index 0000000..c71a424
--- /dev/null
+++ b/tests/unit/test_output.py
@@ -0,0 +1,31 @@
+import io, json, unittest
+from contextlib import redirect_stdout
+from firefly_cli.output import unwrap, emit
+
+class TestOutput(unittest.TestCase):
+ def test_unwrap_list_returns_clean_objects(self):
+ resp = {"data": [
+ {"id": "1", "type": "accounts", "attributes": {"name": "Checking"}},
+ {"id": "2", "type": "accounts", "attributes": {"name": "Savings"}},
+ ]}
+ self.assertEqual(unwrap(resp),
+ [{"id": "1", "name": "Checking"}, {"id": "2", "name": "Savings"}])
+
+ def test_unwrap_single_object(self):
+ resp = {"data": {"id": "5", "type": "accounts",
+ "attributes": {"name": "Wallet"}}}
+ self.assertEqual(unwrap(resp), {"id": "5", "name": "Wallet"})
+
+ def test_emit_json_default(self):
+ buf = io.StringIO()
+ with redirect_stdout(buf):
+ emit([{"id": "1", "name": "x"}], human=False)
+ self.assertEqual(json.loads(buf.getvalue()), [{"id": "1", "name": "x"}])
+
+ def test_emit_human_table_contains_values(self):
+ buf = io.StringIO()
+ with redirect_stdout(buf):
+ emit([{"id": "1", "name": "Checking"}], human=True)
+ out = buf.getvalue()
+ self.assertIn("Checking", out)
+ self.assertIn("id", out)