3636from collections .abc import Callable , Generator
3737from contextlib import contextmanager
3838from datetime import datetime
39- from typing import TYPE_CHECKING , Any , Literal , TextIO , TypeVar , cast
40-
39+ from typing import TYPE_CHECKING , Any , Literal , TextIO , TypeVar , cast , Union , List , Dict
40+ import json
4141import platformdirs
4242
4343from craft_cli import errors
4444from craft_cli .printer import Printer
45+ from abc import ABC , abstractmethod
46+ #from .formatters import BaseFormatter
47+
48+ TabularData = Union [List [Dict [str ,Any ]],Dict [str ,Any ]]
49+
50+ class BaseFormatter (ABC ):
51+ @abstractmethod
52+ def format (self ,data :TabularData ,headers :dict [str ,str ] | None = None )-> str :
53+ pass
54+ class JSONFormatter (BaseFormatter ):
55+ """
56+ Format data into JSON.
4557
58+ Example
59+ -------
60+ >>> formatter=JsonFormatter()
61+ >>> formatter.format([{"name":"Alice","age":30})
62+ '{"name":"Alice","age":30}'
63+ """
64+ def format (self ,data : TabularData ,headers : dict [str ,str ] | None = None )-> str :
65+ return json .dumps (data ,indent = 2 ,default = str )
66+ class TableFormatter (BaseFormatter ):
67+ """
68+ Format data into a pretty table.
69+
70+ Example
71+ -------
72+ >>> formatter=TableFormatter()
73+ >>> formatter.format([["Name","Age"],["Alice",30]])
74+ 'Name Age\n Alice 30'
75+ """
76+ def format (self ,data :TabularData ,headers :dict [str ,str ] | None = None )-> str :
77+ """Format a list of rows into a table string."""
78+ if not data :
79+ return "[no data]"
80+ table_headers :list [str ]
81+ if isinstance (data ,list ):
82+ #list_data=cast(List[Dict[str,Any]],data)
83+ all_keys : set [str ]= set ().union (* (row .keys () for row in data ))
84+ table_headers :list [str ]= sorted (list (all_keys ))
85+ rows = [[str (row .get (h ,"" )) for h in table_headers ] for row in data ]
86+ else :
87+ table_headers = ["Key" , "Value" ]
88+ #data_dict=cast(Dict[str,Any],data)
89+ rows = [[str (k ),str (v )] for k ,v in data .items ()]
90+ cols_widths = [max (len (str (item )) for item in col ) for col in zip (* ([table_headers ]+ rows ))]
91+
92+ def format_row (row : list [str ])-> str :
93+ return " | " .join (str (cell ).ljust (width ) for cell , width in zip (row ,cols_widths ))
94+ table = [format_row (table_headers )]
95+ table .append ("-" * sum (cols_widths )+ "---" * (len (table_headers )- 1 ))
96+ table .extend (format_row (row ) for row in rows )
97+ return "\n " .join (table )
98+
99+
46100if TYPE_CHECKING :
47101 from types import TracebackType
48102
@@ -462,6 +516,10 @@ def __init__(self) -> None:
462516 self ._log_handler : _Handler = None # type: ignore[assignment]
463517 self ._streaming_brief = False
464518 self ._docs_base_url : str | None = None
519+ self ._formatters = {
520+ "json" :JSONFormatter (),
521+ "table" :TableFormatter (),
522+ }
465523
466524 def init (
467525 self ,
@@ -888,6 +946,14 @@ def prompt(self, prompt_text: str, *, hide: bool = False) -> str:
888946 if not val :
889947 raise errors .CraftError ("input cannot be empty" )
890948 return val
949+
950+ @_active_guard ()
951+ def data (self ,records : list [dict ], format :str = "table" )-> None :
952+ if format not in self ._formatters :
953+ raise ValueError (f"Unsupported format: { format } " )
954+ formatter = self ._formatters [format ]
955+ formatted_output = formatter .format (records )
956+ self .message (formatted_output )
891957
892958 @property
893959 def log_filepath (self ) -> pathlib .Path :
@@ -905,3 +971,4 @@ def _format_details(details: str) -> str:
905971 if "\n " in details :
906972 return details if details .startswith ("\n " ) else f"\n { details } "
907973 return details
974+
0 commit comments