rich-objects 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Rick Porter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: rich-objects
3
+ Version: 0.1.0
4
+ Summary:
5
+ License-File: LICENSE
6
+ Author: Rick Porter
7
+ Author-email: rickwporter@gmail.com
8
+ Requires-Python: >=3.10
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Classifier: Topic :: Utilities
26
+ Requires-Dist: pyyaml (>=6.0.3,<7.0.0)
27
+ Requires-Dist: rich (>=14.2.0,<15.0.0)
28
+ Description-Content-Type: text/markdown
29
+
30
+ # openapi-spec-tools
31
+
32
+ This is a small set of tools to help provide easy to use, flexible tools for display complex objects for a CLI.
33
+
34
+ ## Getting started
35
+
36
+ The project has been published to PyPi, so you should be able to install it with something like one of the following (depending on how you do Python package management):
37
+ ```terminal
38
+ % pip install rich-objects
39
+ % poetry add rich-objects
40
+ ```
41
+
42
+ The sections below provide a brief description with links to more examples and details.
43
+
44
+ ## Background
45
+
46
+ This module extends the [rich](https://github.com/Textualize/rich) module to provide pretty printing of complex data objects. The most common use case is a CLI that displays the json/yaml data that is returned to a CLI client in several different formats.
47
+
48
+ The easiest way to leverage this library is using the `display()` function. You can provide a `fmt` and `style` to provide different means of displaying the data.
49
+
50
+
51
+ Here are some of the lower level elements:
52
+ * `OutputFormat` and `OutputSyle` are enums suitable to use as a CLI argument to support different displays
53
+ * `RichTable` class is a thin wrapper derived from `rich.Table`. It contains some default formatting for the tables, since it becomes confusing when tables are nested.
54
+ * Added several functions starting with `rich_table_factory()` to create a `RichTable` with appropriate nesting based on the data returned by the data in the object.
55
+ * The `console_factory()` is the default means for printing the output, but this just sets the `rich.Console` width.
56
+
57
+
58
+ ## Examples
59
+
60
+ In general, this can be used in any enviroment where CLI output is used.
61
+
62
+ ### Typer Example
63
+
64
+ Here's a simple Python example to leverage the new code:
65
+ ```Python
66
+ #!/usr/bin/env python3
67
+ from typer import Typer
68
+ from rich_objects import OutputFormat, display
69
+
70
+ DATA = [
71
+ {"name": "sna", "prop1": 1, "prop B": None, "blah": "zay"},
72
+ {
73
+ "name": "foo",
74
+ "prop2": 2,
75
+ "prop B": True,
76
+ },
77
+ {
78
+ "name": "bar",
79
+ 1: "inverse",
80
+ },
81
+ ]
82
+
83
+ app = Typer()
84
+
85
+ @app.command()
86
+ def print_data(
87
+ output_fmt: OutputFormat = OutputFormat.TEXT,
88
+ output_style: OutputStyle = OutputStyle.ALL,
89
+ indent: int = 2,
90
+ ):
91
+ data = DATA # TODO: figure out how to get your data here
92
+ display(data, fmt=output_fmt, style=output_style, indent=indent)
93
+
94
+ if __name__ == "__main__":
95
+ app()
96
+ ```
97
+
98
+ Here's some sample output:
99
+ ```shell
100
+ (.venv) > ./example.py
101
+ ┏━━━━━━┳━━━━━━━━━━━━━━━━┓
102
+ ┃ Name ┃ Properties ┃
103
+ ┡━━━━━━╇━━━━━━━━━━━━━━━━┩
104
+ │ sna │ prop1 1 │
105
+ │ │ prop B None │
106
+ │ │ blah zay │
107
+ ├──────┼────────────────┤
108
+ │ foo │ prop2 2 │
109
+ │ │ prop B True │
110
+ ├──────┼────────────────┤
111
+ │ bar │ 1 inverse │
112
+ └──────┴────────────────┘
113
+ Found 3 items
114
+ (.venv) > ./example.py --output-fmt json
115
+ [
116
+ {
117
+ "name": "sna",
118
+ "prop1": 1,
119
+ "prop B": null,
120
+ "blah": "zay"
121
+ },
122
+ {
123
+ "name": "foo",
124
+ "prop2": 2,
125
+ "prop B": true
126
+ },
127
+ {
128
+ "name": "bar",
129
+ "1": "inverse"
130
+ }
131
+ ]
132
+ (.venv) > ./example.py --output-fmt yaml
133
+ - blah: zay
134
+ name: sna
135
+ prop B: null
136
+ prop1: 1
137
+ - name: foo
138
+ prop B: true
139
+ prop2: 2
140
+ - name: bar
141
+ 1: inverse
142
+
143
+ (.venv) >
144
+ ```
145
+
146
+
147
+ ## Contributing
148
+
149
+ This project is just getting going... More development instructions will be added later. If you have any suggestions, please email Rick directly (rickwporter@gmail.com).
150
+
@@ -0,0 +1,120 @@
1
+ # openapi-spec-tools
2
+
3
+ This is a small set of tools to help provide easy to use, flexible tools for display complex objects for a CLI.
4
+
5
+ ## Getting started
6
+
7
+ The project has been published to PyPi, so you should be able to install it with something like one of the following (depending on how you do Python package management):
8
+ ```terminal
9
+ % pip install rich-objects
10
+ % poetry add rich-objects
11
+ ```
12
+
13
+ The sections below provide a brief description with links to more examples and details.
14
+
15
+ ## Background
16
+
17
+ This module extends the [rich](https://github.com/Textualize/rich) module to provide pretty printing of complex data objects. The most common use case is a CLI that displays the json/yaml data that is returned to a CLI client in several different formats.
18
+
19
+ The easiest way to leverage this library is using the `display()` function. You can provide a `fmt` and `style` to provide different means of displaying the data.
20
+
21
+
22
+ Here are some of the lower level elements:
23
+ * `OutputFormat` and `OutputSyle` are enums suitable to use as a CLI argument to support different displays
24
+ * `RichTable` class is a thin wrapper derived from `rich.Table`. It contains some default formatting for the tables, since it becomes confusing when tables are nested.
25
+ * Added several functions starting with `rich_table_factory()` to create a `RichTable` with appropriate nesting based on the data returned by the data in the object.
26
+ * The `console_factory()` is the default means for printing the output, but this just sets the `rich.Console` width.
27
+
28
+
29
+ ## Examples
30
+
31
+ In general, this can be used in any enviroment where CLI output is used.
32
+
33
+ ### Typer Example
34
+
35
+ Here's a simple Python example to leverage the new code:
36
+ ```Python
37
+ #!/usr/bin/env python3
38
+ from typer import Typer
39
+ from rich_objects import OutputFormat, display
40
+
41
+ DATA = [
42
+ {"name": "sna", "prop1": 1, "prop B": None, "blah": "zay"},
43
+ {
44
+ "name": "foo",
45
+ "prop2": 2,
46
+ "prop B": True,
47
+ },
48
+ {
49
+ "name": "bar",
50
+ 1: "inverse",
51
+ },
52
+ ]
53
+
54
+ app = Typer()
55
+
56
+ @app.command()
57
+ def print_data(
58
+ output_fmt: OutputFormat = OutputFormat.TEXT,
59
+ output_style: OutputStyle = OutputStyle.ALL,
60
+ indent: int = 2,
61
+ ):
62
+ data = DATA # TODO: figure out how to get your data here
63
+ display(data, fmt=output_fmt, style=output_style, indent=indent)
64
+
65
+ if __name__ == "__main__":
66
+ app()
67
+ ```
68
+
69
+ Here's some sample output:
70
+ ```shell
71
+ (.venv) > ./example.py
72
+ ┏━━━━━━┳━━━━━━━━━━━━━━━━┓
73
+ ┃ Name ┃ Properties ┃
74
+ ┡━━━━━━╇━━━━━━━━━━━━━━━━┩
75
+ │ sna │ prop1 1 │
76
+ │ │ prop B None │
77
+ │ │ blah zay │
78
+ ├──────┼────────────────┤
79
+ │ foo │ prop2 2 │
80
+ │ │ prop B True │
81
+ ├──────┼────────────────┤
82
+ │ bar │ 1 inverse │
83
+ └──────┴────────────────┘
84
+ Found 3 items
85
+ (.venv) > ./example.py --output-fmt json
86
+ [
87
+ {
88
+ "name": "sna",
89
+ "prop1": 1,
90
+ "prop B": null,
91
+ "blah": "zay"
92
+ },
93
+ {
94
+ "name": "foo",
95
+ "prop2": 2,
96
+ "prop B": true
97
+ },
98
+ {
99
+ "name": "bar",
100
+ "1": "inverse"
101
+ }
102
+ ]
103
+ (.venv) > ./example.py --output-fmt yaml
104
+ - blah: zay
105
+ name: sna
106
+ prop B: null
107
+ prop1: 1
108
+ - name: foo
109
+ prop B: true
110
+ prop2: 2
111
+ - name: bar
112
+ 1: inverse
113
+
114
+ (.venv) >
115
+ ```
116
+
117
+
118
+ ## Contributing
119
+
120
+ This project is just getting going... More development instructions will be added later. If you have any suggestions, please email Rick directly (rickwporter@gmail.com).
@@ -0,0 +1,97 @@
1
+ [project]
2
+ name = "rich-objects"
3
+ version = "0.1.0"
4
+ description = ""
5
+ authors = [
6
+ {name = "Rick Porter",email = "rickwporter@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "rich (>=14.2.0,<15.0.0)",
12
+ "pyyaml (>=6.0.3,<7.0.0)"
13
+ ]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3 :: Only",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Programming Language :: Python :: 3.14",
27
+ "Topic :: Software Development",
28
+ "Topic :: Software Development :: Libraries",
29
+ "Topic :: Software Development :: Libraries :: Python Modules",
30
+ "Typing :: Typed",
31
+ "Topic :: Utilities",
32
+ ]
33
+
34
+
35
+ [build-system]
36
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
37
+ build-backend = "poetry.core.masonry.api"
38
+
39
+ [tool.poetry.group.dev.dependencies]
40
+ ruff = "^0.14.9"
41
+ pytest = "^9.0.2"
42
+ coverage = "^7.13.0"
43
+
44
+ [tool.ruff]
45
+ line-length = 120
46
+ target-version = "py312"
47
+
48
+ [tool.ruff.lint]
49
+ select = [
50
+ "D", # pydocstyle
51
+ "E", # pycodestyle errors
52
+ "F", # pyflakes
53
+ "I", # isort
54
+ "W", # pycodestyle warnings
55
+ "B", # flake8-bugbear
56
+ "PL", # pylint
57
+ "C4", # flake8-comprehensions
58
+ "N", # PEP8 naming conventions
59
+ ]
60
+ fixable = [
61
+ "D", "E", "I", "W",
62
+ ]
63
+ ignore = [
64
+ "PLR1711", # "useless" returns make the code more readable
65
+ ]
66
+
67
+ [tool.ruff.lint.per-file-ignores]
68
+ # ignore the unused imports in init
69
+ "__init__.py" = ["F401"]
70
+ "tests/*" = [
71
+ "D", # no need for docstrings on tests
72
+ "PLR2004", # magic numbers allowed in tests
73
+ ]
74
+
75
+ [tool.ruff.lint.isort]
76
+ force-single-line = true
77
+ split-on-trailing-comma = true
78
+
79
+ [tool.ruff.lint.pylint]
80
+ max-args = 25
81
+ max-branches = 12
82
+ max-returns = 10
83
+ max-statements = 75
84
+
85
+ [tool.coverage.run]
86
+ data_file = ".coverage"
87
+ source = [
88
+ "rich_objects",
89
+ ]
90
+
91
+ [tool.coverage.report]
92
+ exclude_lines = [
93
+ "pragma: no cover",
94
+ "@overload",
95
+ 'if __name__ == "__main__":',
96
+ ]
97
+
@@ -0,0 +1,9 @@
1
+ """Module for rich display of complex objects (e.g. JSON/dict)."""
2
+
3
+ from rich_objects.console import console_factory
4
+ from rich_objects.display import display
5
+ from rich_objects.display import rich_table_factory
6
+ from rich_objects.enums import OutputFormat
7
+ from rich_objects.enums import OutputStyle
8
+ from rich_objects.rich_table import RichTable
9
+ from rich_objects.table_config import TableConfig
@@ -0,0 +1,24 @@
1
+ """Module containing a factory for generating a rich Console."""
2
+ import os
3
+
4
+ from rich.console import Console
5
+
6
+ TEST_TERMINAL_WIDTH = 100
7
+
8
+
9
+ def console_factory(*args, **kwargs) -> Console:
10
+ """Create/initialize a Console object.
11
+
12
+ A little hacky here... Allow terminal width to be set directly by an environment variable, or
13
+ when detecting that we're testing use a wide terminal to avoid line wrap issues.
14
+ """
15
+ width = kwargs.pop("width", None)
16
+ width_env = os.environ.get("TERMINAL_WIDTH")
17
+ pytest_version = os.environ.get("PYTEST_VERSION")
18
+ if width is not None:
19
+ pass
20
+ elif width_env is not None:
21
+ width = int(width_env)
22
+ elif pytest_version is not None:
23
+ width = TEST_TERMINAL_WIDTH
24
+ return Console(*args, width=width, **kwargs)
@@ -0,0 +1,30 @@
1
+ """Internationalized constants for controlling appearance."""
2
+ from gettext import gettext
3
+
4
+ # allow for i18n/l8n
5
+ ITEMS = gettext("Items")
6
+ PROPERTY = gettext("Property")
7
+ PROPERTIES = gettext("Properties")
8
+ VALUE = gettext("Value")
9
+ VALUES = gettext("Values")
10
+ UNKNOWN = gettext("Unknown")
11
+ FOUND_ITEMS = gettext("Found {} items")
12
+ ELLIPSIS = gettext("...")
13
+
14
+ OBJECT_HEADERS = [PROPERTY, VALUE]
15
+
16
+ KEY_FIELDS = ["name", "id"]
17
+ URL_PREFIXES = ["http://", "https://", "ftp://"]
18
+
19
+ KEY_MAX_LEN = 35
20
+ VALUE_MAX_LEN = 50
21
+ URL_MAX_LEN = 100
22
+
23
+ # this is value used to denote all other properties (not specified in list)
24
+ WILDCARD_COLUMN = '*'
25
+
26
+ DEFAULT_ROW_PROPS = {
27
+ "justify": "left",
28
+ "no_wrap": True,
29
+ "overflow": "ignore",
30
+ }
@@ -0,0 +1,264 @@
1
+ """Implementation for displaying data in a user-friendly fashion."""
2
+ from typing import Any
3
+ from typing import Optional
4
+
5
+ import yaml
6
+ from rich.console import Console
7
+ from rich.markup import escape
8
+
9
+ from rich_objects.console import console_factory
10
+ from rich_objects.constants import ELLIPSIS
11
+ from rich_objects.constants import PROPERTIES
12
+ from rich_objects.constants import WILDCARD_COLUMN
13
+ from rich_objects.enums import OutputFormat
14
+ from rich_objects.enums import OutputStyle
15
+ from rich_objects.rich_table import RichTable
16
+ from rich_objects.table_config import TableConfig
17
+
18
+ # NOTE: the key field of dictionaries are expected to be be `str`, `int`, `float`, but use
19
+ # `Any` readability.
20
+
21
+
22
+ def headerize(s: str) -> str:
23
+ """Create a table header from the provided string."""
24
+ if s == WILDCARD_COLUMN:
25
+ return PROPERTIES
26
+ return s[0].upper() + s[1:]
27
+
28
+
29
+ def _truncate(s: str, max_length: int) -> str:
30
+ """Truncate the provided string to a maximum of max_length (including elipsis)."""
31
+ if len(s) < max_length:
32
+ return s
33
+ return s[: max_length - 3] + ELLIPSIS
34
+
35
+
36
+ def _get_name_key(item: dict[Any, Any], key_fields: list[str]) -> Optional[str]:
37
+ """Attempt to find an identifying value."""
38
+ for k in key_fields:
39
+ key = str(k)
40
+ if key in item:
41
+ return key
42
+
43
+ return None
44
+
45
+
46
+ def _get_other_key(item: dict[Any, Any], name_key: str) -> Optional[str]:
47
+ """Find the "other" key (if there's just one value)."""
48
+ keys = set(item.keys())
49
+ keys.remove(name_key)
50
+ if len(keys) != 1:
51
+ return None
52
+
53
+ return keys.pop()
54
+
55
+
56
+ def _is_url(s: str, url_prefixes: list[str]) -> bool:
57
+ """Rudimentary check for somethingt starting with URL prefix."""
58
+ return any(s.startswith(p) for p in url_prefixes)
59
+
60
+
61
+ def _safe(v: Any) -> str:
62
+ """Convert 'v' to a string that is properly escaped."""
63
+ return escape(str(v))
64
+
65
+
66
+ def _create_list_table(
67
+ items: list[dict[Any, Any]], outer: bool, config: TableConfig
68
+ ) -> RichTable:
69
+ """Create a table from a list of dictionary items.
70
+
71
+ If an identifying "name key" is found (in the first entry), the table will have 2 columns: name, Properties
72
+ If no identifying "name key" is found, the table will be a single column table with the properties.
73
+
74
+ NOTE: nesting is done as needed
75
+ """
76
+ caption = config.items_caption.format(len(items)) if outer else None
77
+ name_key = _get_name_key(items[0], config.key_fields)
78
+ if not name_key:
79
+ # without identifiers just create table with one "Values" column
80
+ table = RichTable(
81
+ config.values_label,
82
+ outer=outer,
83
+ show_lines=True,
84
+ caption=caption,
85
+ row_props=config.row_properties,
86
+ )
87
+ for item in items:
88
+ table.add_row(_table_cell_value(item, config))
89
+ return table
90
+
91
+ # if there's just one property besides the key, use that as the label
92
+ name_label = headerize(name_key)
93
+ other_key = _get_other_key(items[0], name_key)
94
+ if other_key:
95
+ other_name = headerize(other_key)
96
+ fields = [name_label, other_name]
97
+ table = RichTable(
98
+ *fields,
99
+ outer=outer,
100
+ show_lines=True,
101
+ caption=caption,
102
+ row_props=config.row_properties,
103
+ )
104
+ for item in items:
105
+ # id may be an int, so convert to string before truncating
106
+ name = _safe(item.pop(name_key, config.unknown_label))
107
+ body = _table_cell_value(item.get(other_key), config)
108
+ table.add_row(_truncate(name, config.key_max_len), body)
109
+ return table
110
+
111
+ # create a table with identifier in left column, and rest of data in right column
112
+ fields = [name_label, config.properties_label]
113
+ table = RichTable(
114
+ *fields,
115
+ outer=outer,
116
+ show_lines=True,
117
+ caption=caption,
118
+ row_props=config.row_properties,
119
+ )
120
+ for item in items:
121
+ # id may be an int, so convert to string before truncating
122
+ name = _safe(item.pop(name_key, config.unknown_label))
123
+ body = _table_cell_value(item, config)
124
+ table.add_row(_truncate(name, config.key_max_len), body)
125
+
126
+ return table
127
+
128
+
129
+ def _create_object_table(
130
+ obj: dict[Any, Any], outer: bool, config: TableConfig
131
+ ) -> RichTable:
132
+ """Create a table of a dictionary object.
133
+
134
+ NOTE: nesting is done in the right column as needed.
135
+ """
136
+ headers = [config.property_label, config.value_label]
137
+ table = RichTable(
138
+ *headers, outer=outer, show_lines=False, row_props=config.row_properties
139
+ )
140
+ for k, v in obj.items():
141
+ name = _safe(k)
142
+ table.add_row(_truncate(name, config.key_max_len), _table_cell_value(v, config))
143
+
144
+ return table
145
+
146
+
147
+ def _table_cell_value(obj: Any, config: TableConfig) -> Any:
148
+ """Create the "inner" value for a table cell.
149
+
150
+ Depending on the input value type, the cell may look different. If a dict, or list[dict],
151
+ an inner table is created. Otherwise, the object is converted to a printable value.
152
+ """
153
+ value: Any = None
154
+ if isinstance(obj, dict):
155
+ value = _create_object_table(obj, outer=False, config=config)
156
+ elif isinstance(obj, list) and obj:
157
+ if isinstance(obj[0], dict):
158
+ value = _create_list_table(obj, outer=False, config=config)
159
+ else:
160
+ values = [str(x) for x in obj]
161
+ s = _safe(", ".join(values))
162
+ value = _truncate(s, config.value_max_len)
163
+ else:
164
+ s = _safe(obj)
165
+ max_len = (
166
+ config.url_max_len
167
+ if _is_url(s, config.url_prefixes)
168
+ else config.value_max_len
169
+ )
170
+ value = _truncate(s, max_len)
171
+
172
+ return value
173
+
174
+
175
+ def _create_list_columns_table(items: list[dict[str, Any]], columns: list[str], config: TableConfig) -> RichTable:
176
+ """Create a table with the provided columns."""
177
+ headers = [headerize(c) for c in columns]
178
+ table = RichTable(
179
+ *headers, outer=True, show_lines=True, row_props=config.row_properties
180
+ )
181
+ for item in items:
182
+ values = []
183
+ for c in columns:
184
+ if c == WILDCARD_COLUMN:
185
+ sub_value = {k: v for k, v in item.items() if k not in columns}
186
+ values.append(_table_cell_value(sub_value, config))
187
+ continue
188
+ values.append(_table_cell_value(item.get(c), config))
189
+ table.add_row(*values)
190
+
191
+ return table
192
+
193
+
194
+ def rich_table_factory(
195
+ obj: Any,
196
+ config: Optional[TableConfig] = None,
197
+ columns: Optional[list[str]] = None,
198
+ ) -> RichTable:
199
+ """Create a RichTable (alias for rich.table.Table) from the object."""
200
+ config = config or TableConfig()
201
+ if isinstance(obj, dict):
202
+ return _create_object_table(obj, outer=True, config=config)
203
+
204
+ if isinstance(obj, list) and obj and isinstance(obj[0], dict):
205
+ if columns:
206
+ return _create_list_columns_table(obj, columns=columns, config=config)
207
+
208
+ return _create_list_table(obj, outer=True, config=config)
209
+
210
+ # this is a list of "simple" properties
211
+ if (
212
+ isinstance(obj, list)
213
+ and obj
214
+ and all(
215
+ item is None or isinstance(item, (str, float, bool, int)) for item in obj
216
+ )
217
+ ):
218
+ caption = config.items_caption.format(len(obj))
219
+ table = RichTable(
220
+ config.items_label,
221
+ outer=True,
222
+ show_lines=True,
223
+ caption=caption,
224
+ row_props=config.row_properties,
225
+ )
226
+ for item in obj:
227
+ table.add_row(_table_cell_value(item, config))
228
+ return table
229
+
230
+ raise ValueError(f"Unable to create table for type {type(obj).__name__}")
231
+
232
+
233
+ def display(
234
+ obj: Any,
235
+ fmt: OutputFormat,
236
+ style: OutputStyle,
237
+ indent: int = 2,
238
+ columns: Optional[list[str]] = None,
239
+ console: Optional[Console] = None,
240
+ ) -> None:
241
+ """Display the data provided in obj, according to the formating arguments."""
242
+ no_color = style != OutputStyle.ALL
243
+ highlight = style != OutputStyle.NONE
244
+ console = console or console_factory(no_color=no_color, highlight=highlight)
245
+
246
+ if isinstance(obj, str):
247
+ console.print(_safe(obj))
248
+ return
249
+
250
+ if fmt == OutputFormat.JSON:
251
+ console.print_json(data=obj, indent=indent, highlight=highlight)
252
+ return
253
+
254
+ if fmt == OutputFormat.YAML:
255
+ console.print(_safe(yaml.dump(obj, indent=indent)))
256
+ return
257
+
258
+ if not obj:
259
+ console.print("Nothing found")
260
+ return
261
+
262
+ table = rich_table_factory(obj, columns=columns)
263
+ console.print(table)
264
+ return
@@ -0,0 +1,20 @@
1
+ """Simple enum definitions."""
2
+ from enum import Enum
3
+
4
+
5
+ class OutputFormat(str, Enum):
6
+ """Output text format for received data."""
7
+
8
+ TABLE = "table"
9
+ JSON = "json"
10
+ YAML = "yaml"
11
+
12
+
13
+ class OutputStyle(str, Enum):
14
+ """Text style options for none, bold-only, or bold-and-color."""
15
+
16
+ NONE = "none"
17
+ BOLD = "bold"
18
+ ALL = "all"
19
+
20
+
@@ -0,0 +1,40 @@
1
+ """Contains the RichTable class."""
2
+ from typing import Any
3
+
4
+ from rich.box import HEAVY_HEAD
5
+ from rich.table import Table
6
+
7
+ from rich_objects.constants import DEFAULT_ROW_PROPS
8
+
9
+
10
+ class RichTable(Table):
11
+ """Wrapper for the rich.Table to provide some methods for adding complex items."""
12
+
13
+ def __init__(
14
+ self,
15
+ *args: Any,
16
+ outer: bool = True,
17
+ row_props: dict[str, Any] = DEFAULT_ROW_PROPS,
18
+ **kwargs: Any,
19
+ ):
20
+ """Initialize the Table with a few defaults."""
21
+ super().__init__(
22
+ # items with "regular" defaults
23
+ highlight=kwargs.pop("highlight", True),
24
+ row_styles=kwargs.pop("row_styles", None),
25
+ expand=kwargs.pop("expand", False),
26
+ caption_justify=kwargs.pop("caption_justify", "left"),
27
+ border_style=kwargs.pop("border_style", None),
28
+ leading=kwargs.pop(
29
+ "leading", 0
30
+ ), # warning: setting to non-zero disables lines
31
+ # these items take queues from `outer`
32
+ show_header=kwargs.pop("show_header", outer),
33
+ show_edge=kwargs.pop("show_edge", outer),
34
+ box=HEAVY_HEAD if outer else None,
35
+ **kwargs,
36
+ )
37
+ for name in args:
38
+ self.add_column(name, **row_props)
39
+
40
+
@@ -0,0 +1,40 @@
1
+ """Contains TableConfig class which controlls the table outputs."""
2
+ from dataclasses import dataclass
3
+ from dataclasses import field
4
+ from typing import Any
5
+
6
+ from rich_objects.constants import DEFAULT_ROW_PROPS
7
+ from rich_objects.constants import FOUND_ITEMS
8
+ from rich_objects.constants import ITEMS
9
+ from rich_objects.constants import KEY_FIELDS
10
+ from rich_objects.constants import KEY_MAX_LEN
11
+ from rich_objects.constants import PROPERTIES
12
+ from rich_objects.constants import PROPERTY
13
+ from rich_objects.constants import UNKNOWN
14
+ from rich_objects.constants import URL_MAX_LEN
15
+ from rich_objects.constants import URL_PREFIXES
16
+ from rich_objects.constants import VALUE
17
+ from rich_objects.constants import VALUE_MAX_LEN
18
+ from rich_objects.constants import VALUES
19
+
20
+
21
+ @dataclass
22
+ class TableConfig:
23
+ """Configuration for customizing the table outputs.
24
+
25
+ The defaults provide a standard look and feel, but can be overridden to all customization.
26
+ """
27
+
28
+ items_label: str = ITEMS
29
+ property_label: str = PROPERTY
30
+ properties_label: str = PROPERTIES
31
+ value_label: str = VALUE
32
+ values_label: str = VALUES
33
+ unknown_label: str = UNKNOWN
34
+ items_caption: str = FOUND_ITEMS
35
+ url_prefixes: list[str] = field(default_factory=lambda: URL_PREFIXES)
36
+ url_max_len: int = URL_MAX_LEN
37
+ key_fields: list[str] = field(default_factory=lambda: KEY_FIELDS)
38
+ key_max_len: int = KEY_MAX_LEN
39
+ value_max_len: int = VALUE_MAX_LEN
40
+ row_properties: dict[str, Any] = field(default_factory=lambda: DEFAULT_ROW_PROPS)