openapi-spec-tools 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.
- openapi_spec_tools-0.1.0/LICENSE +21 -0
- openapi_spec_tools-0.1.0/PKG-INFO +45 -0
- openapi_spec_tools-0.1.0/README.md +25 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/__init__.py +10 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/_typer.py +18 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/_arguments.py +101 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/_console.py +25 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/_display.py +309 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/_exceptions.py +22 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/_logging.py +24 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/_requests.py +220 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/_tree.py +170 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/cli.py +425 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/constants.py +2 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/generate.py +183 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/generator.py +837 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/layout.py +247 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/layout_types.py +90 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/cli_gen/utils.py +45 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/oas.py +638 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/types.py +52 -0
- openapi_spec_tools-0.1.0/openapi_spec_tools/utils.py +513 -0
- openapi_spec_tools-0.1.0/pyproject.toml +75 -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,45 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: openapi-spec-tools
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenAPI specification tools for analyzing, updating, and generating a CLI.
|
|
5
|
+
Author: Rick Porter
|
|
6
|
+
Author-email: rickwporter@gmail.com
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
|
15
|
+
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
|
16
|
+
Requires-Dist: rich (>=13.9.4,<14.0.0)
|
|
17
|
+
Requires-Dist: typer (>=0.15.1,<0.16.0)
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# oas-tools
|
|
21
|
+
|
|
22
|
+
Welcome to OpenAPI specification (OAS) tools!
|
|
23
|
+
|
|
24
|
+
This is a collection of tools for using OpenAPI specifications. The OpenAPI community has a plethora of tools, and this is intended to supplement those. The tools here provide functionality that has not been readily available elsewhere.
|
|
25
|
+
|
|
26
|
+
## OAS
|
|
27
|
+
|
|
28
|
+
The `oas` script provides a tool for analyzing and modifying an OpenAPI spec. See [OAS.md](OAS.md) for more info.
|
|
29
|
+
|
|
30
|
+
## CLI Generation
|
|
31
|
+
|
|
32
|
+
The `cli-gen` tool allows users to create a user-friendly CLI using the OpenAPI spec and a layout file. The layout file provides the CLI structure and refers to the OpenAPI spec for details of operations. [LAYOUT.md](LAYOUT.md) has more details about the layout file, and the [CLI_GEN.md](CLI_GEN.md) has more info about CLI generation.
|
|
33
|
+
|
|
34
|
+
See the examples in `examples/` for some more complete works.
|
|
35
|
+
|
|
36
|
+
## client.mk
|
|
37
|
+
|
|
38
|
+
The `client.mk` file is an example of a `Makefile` to invoke the [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator) via a container. The file can be copied/modified to be invoked with an OpenAPI specfication (other than `openapi.yaml`) and a real package name. For a more complete list of generator options, look at the [OpenAPI generator usage documentation](https://openapi-generator.tech/docs/usage#generate).
|
|
39
|
+
|
|
40
|
+
## Contributing
|
|
41
|
+
|
|
42
|
+
The [DEVELOPMENT.md](DEVELOPMENT.md) has more information about getting setup as a developer.
|
|
43
|
+
|
|
44
|
+
The [TODO.md](TODO.md) has some ideas where this project can be improved and expanded -- please add your ideas here, or email Rick directly (rickwporter@gmail.com).
|
|
45
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# oas-tools
|
|
2
|
+
|
|
3
|
+
Welcome to OpenAPI specification (OAS) tools!
|
|
4
|
+
|
|
5
|
+
This is a collection of tools for using OpenAPI specifications. The OpenAPI community has a plethora of tools, and this is intended to supplement those. The tools here provide functionality that has not been readily available elsewhere.
|
|
6
|
+
|
|
7
|
+
## OAS
|
|
8
|
+
|
|
9
|
+
The `oas` script provides a tool for analyzing and modifying an OpenAPI spec. See [OAS.md](OAS.md) for more info.
|
|
10
|
+
|
|
11
|
+
## CLI Generation
|
|
12
|
+
|
|
13
|
+
The `cli-gen` tool allows users to create a user-friendly CLI using the OpenAPI spec and a layout file. The layout file provides the CLI structure and refers to the OpenAPI spec for details of operations. [LAYOUT.md](LAYOUT.md) has more details about the layout file, and the [CLI_GEN.md](CLI_GEN.md) has more info about CLI generation.
|
|
14
|
+
|
|
15
|
+
See the examples in `examples/` for some more complete works.
|
|
16
|
+
|
|
17
|
+
## client.mk
|
|
18
|
+
|
|
19
|
+
The `client.mk` file is an example of a `Makefile` to invoke the [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator) via a container. The file can be copied/modified to be invoked with an OpenAPI specfication (other than `openapi.yaml`) and a real package name. For a more complete list of generator options, look at the [OpenAPI generator usage documentation](https://openapi-generator.tech/docs/usage#generate).
|
|
20
|
+
|
|
21
|
+
## Contributing
|
|
22
|
+
|
|
23
|
+
The [DEVELOPMENT.md](DEVELOPMENT.md) has more information about getting setup as a developer.
|
|
24
|
+
|
|
25
|
+
The [TODO.md](TODO.md) has some ideas where this project can be improved and expanded -- please add your ideas here, or email Rick directly (rickwporter@gmail.com).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from openapi_spec_tools.utils import count_values
|
|
2
|
+
from openapi_spec_tools.utils import find_diffs
|
|
3
|
+
from openapi_spec_tools.utils import find_paths
|
|
4
|
+
from openapi_spec_tools.utils import find_references
|
|
5
|
+
from openapi_spec_tools.utils import map_operations
|
|
6
|
+
from openapi_spec_tools.utils import open_oas
|
|
7
|
+
from openapi_spec_tools.utils import remove_schema_tags
|
|
8
|
+
from openapi_spec_tools.utils import schema_operations_filter
|
|
9
|
+
from openapi_spec_tools.utils import set_nullable_not_required
|
|
10
|
+
from openapi_spec_tools.utils import unroll
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This provides some common extensions to Typer.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print
|
|
7
|
+
from typing_extensions import Annotated
|
|
8
|
+
|
|
9
|
+
# Common argument definition
|
|
10
|
+
OasFilenameArgument = Annotated[str, typer.Argument(show_default=False, help="OpenAPI specification file")]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def error_out(message: str, exit_code: int = 1) -> None:
|
|
14
|
+
"""Utility to print provided error message (with red ERROR prefix) and exit"""
|
|
15
|
+
print(f"[red]ERROR:[/red] {message}")
|
|
16
|
+
raise typer.Exit(exit_code)
|
|
17
|
+
|
|
18
|
+
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from typing_extensions import Annotated
|
|
5
|
+
|
|
6
|
+
from openapi_spec_tools.cli_gen._display import OutputFormat
|
|
7
|
+
from openapi_spec_tools.cli_gen._display import OutputStyle
|
|
8
|
+
from openapi_spec_tools.cli_gen._logging import LogLevel
|
|
9
|
+
from openapi_spec_tools.cli_gen._tree import TreeDisplay
|
|
10
|
+
|
|
11
|
+
ENV_API_HOST = "API_HOST"
|
|
12
|
+
ENV_API_KEY = "API_KEY"
|
|
13
|
+
ENV_API_TIME = "API_TIMEOUT"
|
|
14
|
+
ENV_LOG_LEVEL = "LOG_LEVEL"
|
|
15
|
+
ENV_OUT_FORMAT = "OUTPUT_FORMAT"
|
|
16
|
+
ENV_OUT_STYLE = "OUTPUT_STYLE"
|
|
17
|
+
|
|
18
|
+
ApiKeyOption = Annotated[
|
|
19
|
+
str,
|
|
20
|
+
typer.Option(
|
|
21
|
+
"--api-key",
|
|
22
|
+
show_default=False,
|
|
23
|
+
envvar=ENV_API_KEY,
|
|
24
|
+
help="API key for authentication",
|
|
25
|
+
),
|
|
26
|
+
]
|
|
27
|
+
ApiHostOption = Annotated[
|
|
28
|
+
str,
|
|
29
|
+
typer.Option(
|
|
30
|
+
"--api-host",
|
|
31
|
+
show_default=False,
|
|
32
|
+
envvar=ENV_API_HOST,
|
|
33
|
+
help="API host address",
|
|
34
|
+
),
|
|
35
|
+
]
|
|
36
|
+
ApiTimeoutOption = Annotated[
|
|
37
|
+
int,
|
|
38
|
+
typer.Option(
|
|
39
|
+
"--api-timeout",
|
|
40
|
+
envvar=ENV_API_TIME,
|
|
41
|
+
help="API request timeout in seconds for a single request",
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
DetailsOption = Annotated[
|
|
45
|
+
bool,
|
|
46
|
+
typer.Option(
|
|
47
|
+
"--details/--summary",
|
|
48
|
+
"-v",
|
|
49
|
+
help="Display the full details or a summary."
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
LogLevelOption = Annotated[
|
|
53
|
+
LogLevel,
|
|
54
|
+
typer.Option(
|
|
55
|
+
"--log",
|
|
56
|
+
case_sensitive=False,
|
|
57
|
+
envvar=ENV_LOG_LEVEL,
|
|
58
|
+
help="Log level",
|
|
59
|
+
),
|
|
60
|
+
]
|
|
61
|
+
MaxDepthOption = Annotated[
|
|
62
|
+
int,
|
|
63
|
+
typer.Option(
|
|
64
|
+
"--depth",
|
|
65
|
+
"--max-depth",
|
|
66
|
+
help="Maximum depth of tree to display."
|
|
67
|
+
),
|
|
68
|
+
]
|
|
69
|
+
MaxCountOption = Annotated[
|
|
70
|
+
Optional[int],
|
|
71
|
+
typer.Option(
|
|
72
|
+
"--max",
|
|
73
|
+
"--max-count",
|
|
74
|
+
help="Maximum number of items to get (if any)."
|
|
75
|
+
)
|
|
76
|
+
]
|
|
77
|
+
OutputFormatOption = Annotated[
|
|
78
|
+
OutputFormat,
|
|
79
|
+
typer.Option(
|
|
80
|
+
"--format",
|
|
81
|
+
case_sensitive=False,
|
|
82
|
+
envvar=ENV_OUT_FORMAT,
|
|
83
|
+
help="Output format style",
|
|
84
|
+
),
|
|
85
|
+
]
|
|
86
|
+
OutputStyleOption = Annotated[
|
|
87
|
+
OutputStyle,
|
|
88
|
+
typer.Option(
|
|
89
|
+
"--style",
|
|
90
|
+
case_sensitive=False,
|
|
91
|
+
envvar=ENV_OUT_STYLE,
|
|
92
|
+
help="Style for output",
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
TreeDisplayOption = Annotated[
|
|
96
|
+
TreeDisplay,
|
|
97
|
+
typer.Option(
|
|
98
|
+
case_sensitive=False,
|
|
99
|
+
help="Details of the CLI command tree to show."
|
|
100
|
+
),
|
|
101
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
TEST_TERMINAL_WIDTH = 100
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def console_factory(*args, **kwargs) -> Console:
|
|
9
|
+
"""Utility to consolidate creation/initialization of Console.
|
|
10
|
+
|
|
11
|
+
A little hacky here... Allow terminal width to be set directly by an environment variable, or
|
|
12
|
+
when detecting that we're testing use a wide terminal to avoid line wrap issues.
|
|
13
|
+
"""
|
|
14
|
+
width = kwargs.pop("width", None)
|
|
15
|
+
width_env = os.environ.get("TERMINAL_WIDTH")
|
|
16
|
+
pytest_version = os.environ.get("PYTEST_VERSION")
|
|
17
|
+
if width is not None:
|
|
18
|
+
pass
|
|
19
|
+
elif width_env is not None:
|
|
20
|
+
width = int(width_env)
|
|
21
|
+
elif pytest_version is not None:
|
|
22
|
+
width = TEST_TERMINAL_WIDTH
|
|
23
|
+
return Console(*args, width=width, **kwargs)
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from gettext import gettext
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from rich.box import HEAVY_HEAD
|
|
8
|
+
from rich.markup import escape
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from openapi_spec_tools.cli_gen._console import console_factory
|
|
12
|
+
|
|
13
|
+
DEFAULT_ROW_PROPS = {
|
|
14
|
+
"justify": "left",
|
|
15
|
+
"no_wrap": True,
|
|
16
|
+
"overflow": "ignore",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# allow for i18n/l8n
|
|
20
|
+
ITEMS = gettext("Items")
|
|
21
|
+
PROPERTY = gettext("Property")
|
|
22
|
+
PROPERTIES = gettext("Properties")
|
|
23
|
+
VALUE = gettext("Value")
|
|
24
|
+
VALUES = gettext("Values")
|
|
25
|
+
UNKNOWN = gettext("Unknown")
|
|
26
|
+
FOUND_ITEMS = gettext("Found {} items")
|
|
27
|
+
ELLIPSIS = gettext("...")
|
|
28
|
+
|
|
29
|
+
OBJECT_HEADERS = [PROPERTY, VALUE]
|
|
30
|
+
|
|
31
|
+
KEY_FIELDS = ["name", "id"]
|
|
32
|
+
URL_PREFIXES = ["http://", "https://", "ftp://"]
|
|
33
|
+
|
|
34
|
+
KEY_MAX_LEN = 35
|
|
35
|
+
VALUE_MAX_LEN = 50
|
|
36
|
+
URL_MAX_LEN = 100
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# NOTE: the key field of dictionaries are expected to be be `str`, `int`, `float`, but use
|
|
40
|
+
# `Any` readability.
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TableConfig:
|
|
44
|
+
"""This data class provides a means for customizing the table outputs.
|
|
45
|
+
|
|
46
|
+
The defaults provide a standard look and feel, but can be overridden to all customization.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
items_label: str = ITEMS,
|
|
52
|
+
property_label: str = PROPERTY,
|
|
53
|
+
properties_label: str = PROPERTIES,
|
|
54
|
+
value_label: str = VALUE,
|
|
55
|
+
values_label: str = VALUES,
|
|
56
|
+
unknown_label: str = UNKNOWN,
|
|
57
|
+
items_caption: str = FOUND_ITEMS,
|
|
58
|
+
url_prefixes: list[str] = URL_PREFIXES,
|
|
59
|
+
url_max_len: int = URL_MAX_LEN,
|
|
60
|
+
key_fields: list[str] = KEY_FIELDS,
|
|
61
|
+
key_max_len: int = KEY_MAX_LEN,
|
|
62
|
+
value_max_len: int = VALUE_MAX_LEN,
|
|
63
|
+
row_properties: dict[str, Any] = DEFAULT_ROW_PROPS,
|
|
64
|
+
):
|
|
65
|
+
self.items_label = items_label
|
|
66
|
+
self.property_label = property_label
|
|
67
|
+
self.properties_label = properties_label
|
|
68
|
+
self.value_label = value_label
|
|
69
|
+
self.values_label = values_label
|
|
70
|
+
self.unknown_label = unknown_label
|
|
71
|
+
self.items_caption = items_caption
|
|
72
|
+
self.url_prefixes = url_prefixes
|
|
73
|
+
self.url_max_len = url_max_len
|
|
74
|
+
self.key_fields = key_fields
|
|
75
|
+
self.key_max_len = key_max_len
|
|
76
|
+
self.value_max_len = value_max_len
|
|
77
|
+
self.row_properties = row_properties
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class RichTable(Table):
|
|
81
|
+
"""
|
|
82
|
+
This is wrapper around the rich.Table to provide some methods for adding complex items.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*args: Any,
|
|
88
|
+
outer: bool = True,
|
|
89
|
+
row_props: dict[str, Any] = DEFAULT_ROW_PROPS,
|
|
90
|
+
**kwargs: Any,
|
|
91
|
+
):
|
|
92
|
+
super().__init__(
|
|
93
|
+
# items with "regular" defaults
|
|
94
|
+
highlight=kwargs.pop("highlight", True),
|
|
95
|
+
row_styles=kwargs.pop("row_styles", None),
|
|
96
|
+
expand=kwargs.pop("expand", False),
|
|
97
|
+
caption_justify=kwargs.pop("caption_justify", "left"),
|
|
98
|
+
border_style=kwargs.pop("border_style", None),
|
|
99
|
+
leading=kwargs.pop(
|
|
100
|
+
"leading", 0
|
|
101
|
+
), # warning: setting to non-zero disables lines
|
|
102
|
+
# these items take queues from `outer`
|
|
103
|
+
show_header=kwargs.pop("show_header", outer),
|
|
104
|
+
show_edge=kwargs.pop("show_edge", outer),
|
|
105
|
+
box=HEAVY_HEAD if outer else None,
|
|
106
|
+
**kwargs,
|
|
107
|
+
)
|
|
108
|
+
for name in args:
|
|
109
|
+
self.add_column(name, **row_props)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _truncate(s: str, max_length: int) -> str:
|
|
113
|
+
"""Truncates the provided string to a maximum of max_length (including elipsis)"""
|
|
114
|
+
if len(s) < max_length:
|
|
115
|
+
return s
|
|
116
|
+
return s[: max_length - 3] + ELLIPSIS
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _get_name_key(item: dict[Any, Any], key_fields: list[str]) -> Optional[str]:
|
|
120
|
+
"""Attempts to find an identifying value."""
|
|
121
|
+
for k in key_fields:
|
|
122
|
+
key = str(k)
|
|
123
|
+
if key in item:
|
|
124
|
+
return key
|
|
125
|
+
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _is_url(s: str, url_prefixes: list[str]) -> bool:
|
|
130
|
+
"""Rudimentary check for somethingt starting with URL prefix"""
|
|
131
|
+
return any(s.startswith(p) for p in url_prefixes)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _safe(v: Any) -> str:
|
|
135
|
+
"""Converts 'v' to a string that is properly escaped."""
|
|
136
|
+
return escape(str(v))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _create_list_table(
|
|
140
|
+
items: list[dict[Any, Any]], outer: bool, config: TableConfig
|
|
141
|
+
) -> RichTable:
|
|
142
|
+
"""Creates a table from a list of dictionary items.
|
|
143
|
+
|
|
144
|
+
If an identifying "name key" is found (in the first entry), the table will have 2 columns: name, Properties
|
|
145
|
+
If no identifying "name key" is found, the table will be a single column table with the properties.
|
|
146
|
+
|
|
147
|
+
NOTE: nesting is done as needed
|
|
148
|
+
"""
|
|
149
|
+
caption = config.items_caption.format(len(items)) if outer else None
|
|
150
|
+
name_key = _get_name_key(items[0], config.key_fields)
|
|
151
|
+
if not name_key:
|
|
152
|
+
# without identifiers just create table with one "Values" column
|
|
153
|
+
table = RichTable(
|
|
154
|
+
config.values_label,
|
|
155
|
+
outer=outer,
|
|
156
|
+
show_lines=True,
|
|
157
|
+
caption=caption,
|
|
158
|
+
row_props=config.row_properties,
|
|
159
|
+
)
|
|
160
|
+
for item in items:
|
|
161
|
+
table.add_row(_table_cell_value(item, config))
|
|
162
|
+
return table
|
|
163
|
+
|
|
164
|
+
# create a table with identifier in left column, and rest of data in right column
|
|
165
|
+
name_label = name_key[0].upper() + name_key[1:]
|
|
166
|
+
fields = [name_label, config.properties_label]
|
|
167
|
+
table = RichTable(
|
|
168
|
+
*fields,
|
|
169
|
+
outer=outer,
|
|
170
|
+
show_lines=True,
|
|
171
|
+
caption=caption,
|
|
172
|
+
row_props=config.row_properties,
|
|
173
|
+
)
|
|
174
|
+
for item in items:
|
|
175
|
+
# id may be an int, so convert to string before truncating
|
|
176
|
+
name = _safe(item.pop(name_key, config.unknown_label))
|
|
177
|
+
body = _table_cell_value(item, config)
|
|
178
|
+
table.add_row(_truncate(name, config.key_max_len), body)
|
|
179
|
+
|
|
180
|
+
return table
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _create_object_table(
|
|
184
|
+
obj: dict[Any, Any], outer: bool, config: TableConfig
|
|
185
|
+
) -> RichTable:
|
|
186
|
+
"""Creates a table of a dictionary object.
|
|
187
|
+
|
|
188
|
+
NOTE: nesting is done in the right column as needed.
|
|
189
|
+
"""
|
|
190
|
+
headers = [config.property_label, config.value_label]
|
|
191
|
+
table = RichTable(
|
|
192
|
+
*headers, outer=outer, show_lines=False, row_props=config.row_properties
|
|
193
|
+
)
|
|
194
|
+
for k, v in obj.items():
|
|
195
|
+
name = _safe(k)
|
|
196
|
+
table.add_row(_truncate(name, config.key_max_len), _table_cell_value(v, config))
|
|
197
|
+
|
|
198
|
+
return table
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _table_cell_value(obj: Any, config: TableConfig) -> Any:
|
|
202
|
+
"""Creates the "inner" value for a table cell.
|
|
203
|
+
|
|
204
|
+
Depending on the input value type, the cell may look different. If a dict, or list[dict],
|
|
205
|
+
an inner table is created. Otherwise, the object is converted to a printable value.
|
|
206
|
+
"""
|
|
207
|
+
value: Any = None
|
|
208
|
+
if isinstance(obj, dict):
|
|
209
|
+
value = _create_object_table(obj, outer=False, config=config)
|
|
210
|
+
elif isinstance(obj, list) and obj:
|
|
211
|
+
if isinstance(obj[0], dict):
|
|
212
|
+
value = _create_list_table(obj, outer=False, config=config)
|
|
213
|
+
else:
|
|
214
|
+
values = [str(x) for x in obj]
|
|
215
|
+
s = _safe(", ".join(values))
|
|
216
|
+
value = _truncate(s, config.value_max_len)
|
|
217
|
+
else:
|
|
218
|
+
s = _safe(obj)
|
|
219
|
+
max_len = (
|
|
220
|
+
config.url_max_len
|
|
221
|
+
if _is_url(s, config.url_prefixes)
|
|
222
|
+
else config.value_max_len
|
|
223
|
+
)
|
|
224
|
+
value = _truncate(s, max_len)
|
|
225
|
+
|
|
226
|
+
return value
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def rich_table_factory(obj: Any, config: TableConfig = TableConfig()) -> RichTable:
|
|
230
|
+
"""Create a RichTable (alias for rich.table.Table) from the object."""
|
|
231
|
+
if isinstance(obj, dict):
|
|
232
|
+
return _create_object_table(obj, outer=True, config=config)
|
|
233
|
+
|
|
234
|
+
if isinstance(obj, list) and obj and isinstance(obj[0], dict):
|
|
235
|
+
return _create_list_table(obj, outer=True, config=config)
|
|
236
|
+
|
|
237
|
+
# this is a list of "simple" properties
|
|
238
|
+
if (
|
|
239
|
+
isinstance(obj, list)
|
|
240
|
+
and obj
|
|
241
|
+
and all(
|
|
242
|
+
item is None or isinstance(item, (str, float, bool, int)) for item in obj
|
|
243
|
+
)
|
|
244
|
+
):
|
|
245
|
+
caption = config.items_caption.format(len(obj))
|
|
246
|
+
table = RichTable(
|
|
247
|
+
config.items_label,
|
|
248
|
+
outer=True,
|
|
249
|
+
show_lines=True,
|
|
250
|
+
caption=caption,
|
|
251
|
+
row_props=config.row_properties,
|
|
252
|
+
)
|
|
253
|
+
for item in obj:
|
|
254
|
+
table.add_row(_table_cell_value(item, config))
|
|
255
|
+
return table
|
|
256
|
+
|
|
257
|
+
raise ValueError(f"Unable to create table for type {type(obj).__name__}")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
###################################################################################################
|
|
261
|
+
# Below will remain even after the code from https://github.com/fastapi/typer/pull/1099 merges.
|
|
262
|
+
###################################################################################################
|
|
263
|
+
class OutputFormat(str, Enum):
|
|
264
|
+
TABLE = "table"
|
|
265
|
+
JSON = "json"
|
|
266
|
+
YAML = "yaml"
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class OutputStyle(str, Enum):
|
|
270
|
+
NONE = "none"
|
|
271
|
+
BOLD = "bold"
|
|
272
|
+
ALL = "all"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def summary(obj: Any, properties: list[str]) -> Any:
|
|
276
|
+
"""Gets the item with just the specified properties."""
|
|
277
|
+
if obj is None:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
if isinstance(obj, list):
|
|
281
|
+
# recursively call for each object in list
|
|
282
|
+
return [summary(item, properties) for item in obj]
|
|
283
|
+
|
|
284
|
+
return {prop: obj.get(prop) for prop in properties}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def display(obj: Any, fmt: OutputFormat, style: OutputStyle, indent: int = 2) -> None:
|
|
288
|
+
"""
|
|
289
|
+
This function handles display of the data provided in obj, according to the formating arguments.
|
|
290
|
+
"""
|
|
291
|
+
no_color = style != OutputStyle.ALL
|
|
292
|
+
highlight = style != OutputStyle.NONE
|
|
293
|
+
console = console_factory(no_color=no_color, highlight=highlight)
|
|
294
|
+
|
|
295
|
+
if fmt == OutputFormat.JSON:
|
|
296
|
+
console.print_json(data=obj, indent=indent, highlight=highlight)
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
if fmt == OutputFormat.YAML:
|
|
300
|
+
console.print(_safe(yaml.dump(obj, indent=indent)))
|
|
301
|
+
return
|
|
302
|
+
|
|
303
|
+
if not obj:
|
|
304
|
+
console.print("Nothing found")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
table = rich_table_factory(obj)
|
|
308
|
+
console.print(table)
|
|
309
|
+
return
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from requests import HTTPError
|
|
3
|
+
|
|
4
|
+
from openapi_spec_tools.cli_gen._console import console_factory
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MissingRequiredError(Exception):
|
|
8
|
+
"""Short wrapper to provde feedback about missing required options."""
|
|
9
|
+
def __init__(self, names: list[str]):
|
|
10
|
+
message = f"Missing required parameters, please provide: {', '.join(names)}"
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def handle_exceptions(ex: Exception) -> None:
|
|
15
|
+
"""Process exception and print a more concise error"""
|
|
16
|
+
if isinstance(ex, HTTPError):
|
|
17
|
+
message = str(ex.args[0])
|
|
18
|
+
else:
|
|
19
|
+
message = str(ex)
|
|
20
|
+
console = console_factory()
|
|
21
|
+
console.print(f"[red]ERROR:[/red] {message}")
|
|
22
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
LOG_CLASS = "cli"
|
|
6
|
+
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s %(message)s"
|
|
7
|
+
LOG_DATE_FMT = "%Y-%m-%d %I:%M:%S %p"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogLevel(str, Enum):
|
|
11
|
+
CRITICAL = "critical"
|
|
12
|
+
ERROR = "error"
|
|
13
|
+
WARN = "warn"
|
|
14
|
+
INFO = "info"
|
|
15
|
+
DEBUG = "debug"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def logger(name: Optional[str] = LOG_CLASS) -> logging.Logger:
|
|
19
|
+
return logging.getLogger(name=name)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def init_logging(level: LogLevel, name: Optional[str] = LOG_CLASS):
|
|
23
|
+
logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATE_FMT)
|
|
24
|
+
logger(name).setLevel(level.upper())
|