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.
- rich_objects-0.1.0/LICENSE +21 -0
- rich_objects-0.1.0/PKG-INFO +150 -0
- rich_objects-0.1.0/README.md +120 -0
- rich_objects-0.1.0/pyproject.toml +97 -0
- rich_objects-0.1.0/rich_objects/__init__.py +9 -0
- rich_objects-0.1.0/rich_objects/console.py +24 -0
- rich_objects-0.1.0/rich_objects/constants.py +30 -0
- rich_objects-0.1.0/rich_objects/display.py +264 -0
- rich_objects-0.1.0/rich_objects/enums.py +20 -0
- rich_objects-0.1.0/rich_objects/rich_table.py +40 -0
- rich_objects-0.1.0/rich_objects/table_config.py +40 -0
|
@@ -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)
|