swagger2drawio 1.0.0__py3-none-any.whl
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.
- swagger2drawio/__init__.py +3 -0
- swagger2drawio/_logging.py +73 -0
- swagger2drawio/cli.py +464 -0
- swagger2drawio/config_loader.py +258 -0
- swagger2drawio/diff.py +279 -0
- swagger2drawio/exceptions.py +65 -0
- swagger2drawio/layout/__init__.py +1 -0
- swagger2drawio/layout/graph_engine.py +599 -0
- swagger2drawio/layout/strategies.py +204 -0
- swagger2drawio/parser/__init__.py +1 -0
- swagger2drawio/parser/base.py +87 -0
- swagger2drawio/parser/openapi_parser.py +912 -0
- swagger2drawio/refs.py +261 -0
- swagger2drawio/renderer/__init__.py +1 -0
- swagger2drawio/renderer/diff_renderer.py +470 -0
- swagger2drawio/renderer/drawio_builder.py +224 -0
- swagger2drawio/themes/dark.yaml +43 -0
- swagger2drawio/themes/high-contrast.yaml +43 -0
- swagger2drawio/themes/light.yaml +43 -0
- swagger2drawio/themes/pastel.yaml +43 -0
- swagger2drawio/validator.py +40 -0
- swagger2drawio-1.0.0.dist-info/METADATA +274 -0
- swagger2drawio-1.0.0.dist-info/RECORD +26 -0
- swagger2drawio-1.0.0.dist-info/WHEEL +4 -0
- swagger2drawio-1.0.0.dist-info/entry_points.txt +2 -0
- swagger2drawio-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Logging configuration for the CLI.
|
|
2
|
+
|
|
3
|
+
A single :func:`configure` entrypoint sets up the root ``swagger2drawio``
|
|
4
|
+
logger so that all module-level loggers obtained via
|
|
5
|
+
``logging.getLogger(__name__)`` honour the user's ``-v / -vv / -q /
|
|
6
|
+
--log-file`` choices.
|
|
7
|
+
|
|
8
|
+
Console output goes through Rich's :class:`~rich.logging.RichHandler`
|
|
9
|
+
(colourised, with the module path). File output (when ``--log-file`` is
|
|
10
|
+
set) uses a plain text formatter so the log is grep-friendly.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from rich.logging import RichHandler
|
|
19
|
+
|
|
20
|
+
_ROOT_LOGGER = "swagger2drawio"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def configure(verbosity: int = 0, quiet: bool = False, log_file: Path | None = None) -> None:
|
|
24
|
+
"""Configure the package's root logger.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
verbosity: ``0`` for default (WARNING+ on stderr), ``1`` for
|
|
28
|
+
INFO, ``2+`` for DEBUG.
|
|
29
|
+
quiet: When ``True``, force the console level to WARNING+ even
|
|
30
|
+
if *verbosity* is non-zero.
|
|
31
|
+
log_file: Optional path. When supplied, DEBUG-and-up messages
|
|
32
|
+
are appended to this file regardless of console verbosity.
|
|
33
|
+
"""
|
|
34
|
+
root = logging.getLogger(_ROOT_LOGGER)
|
|
35
|
+
# Reset on every call so repeated CLI invocations in the same
|
|
36
|
+
# process (e.g. inside tests) don't stack handlers.
|
|
37
|
+
for handler in list(root.handlers):
|
|
38
|
+
root.removeHandler(handler)
|
|
39
|
+
|
|
40
|
+
# ── Console handler ──────────────────────────────────────
|
|
41
|
+
if quiet:
|
|
42
|
+
console_level = logging.WARNING
|
|
43
|
+
elif verbosity >= 2:
|
|
44
|
+
console_level = logging.DEBUG
|
|
45
|
+
elif verbosity == 1:
|
|
46
|
+
console_level = logging.INFO
|
|
47
|
+
else:
|
|
48
|
+
console_level = logging.WARNING
|
|
49
|
+
|
|
50
|
+
console = RichHandler(
|
|
51
|
+
rich_tracebacks=True,
|
|
52
|
+
show_path=False,
|
|
53
|
+
markup=False,
|
|
54
|
+
show_time=False,
|
|
55
|
+
show_level=True,
|
|
56
|
+
)
|
|
57
|
+
console.setLevel(console_level)
|
|
58
|
+
console.setFormatter(logging.Formatter("%(message)s"))
|
|
59
|
+
root.addHandler(console)
|
|
60
|
+
|
|
61
|
+
# ── File handler (always DEBUG) ──────────────────────────
|
|
62
|
+
if log_file is not None:
|
|
63
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
file_handler = logging.FileHandler(log_file, encoding="utf-8")
|
|
65
|
+
file_handler.setLevel(logging.DEBUG)
|
|
66
|
+
file_handler.setFormatter(
|
|
67
|
+
logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
68
|
+
)
|
|
69
|
+
root.addHandler(file_handler)
|
|
70
|
+
|
|
71
|
+
# Root level is the minimum so handlers can filter upward.
|
|
72
|
+
root.setLevel(min(console_level, logging.DEBUG if log_file else console_level))
|
|
73
|
+
root.propagate = False
|
swagger2drawio/cli.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""Command-line interface for swagger2drawio.
|
|
2
|
+
|
|
3
|
+
This module defines the Typer application and exposes the ``generate``
|
|
4
|
+
command that drives the full pipeline:
|
|
5
|
+
|
|
6
|
+
1. Load theme / layout configuration (YAML).
|
|
7
|
+
2. Parse an OpenAPI specification (file or URL).
|
|
8
|
+
3. Build NetworkX graphs & compute layout coordinates.
|
|
9
|
+
4. Render the final ``.drawio`` file via *drawpyo*.
|
|
10
|
+
|
|
11
|
+
Usage examples::
|
|
12
|
+
|
|
13
|
+
swagger2drawio generate --input openapi.yaml --output api.drawio
|
|
14
|
+
swagger2drawio generate --input https://petstore3.swagger.io/api/v3/openapi.json
|
|
15
|
+
swagger2drawio generate --input spec.yaml --config my_theme.yaml
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import typer
|
|
24
|
+
|
|
25
|
+
from swagger2drawio import __version__
|
|
26
|
+
from swagger2drawio._logging import configure as configure_logging
|
|
27
|
+
from swagger2drawio.config_loader import (
|
|
28
|
+
list_builtin_themes,
|
|
29
|
+
load_config,
|
|
30
|
+
read_theme,
|
|
31
|
+
)
|
|
32
|
+
from swagger2drawio.exceptions import EXIT_CODE, Swagger2DrawioError
|
|
33
|
+
from swagger2drawio.layout.graph_engine import build_endpoint_graph, build_schema_graph
|
|
34
|
+
from swagger2drawio.parser.openapi_parser import OpenAPIParser
|
|
35
|
+
from swagger2drawio.renderer.drawio_builder import render
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger("swagger2drawio.cli")
|
|
38
|
+
|
|
39
|
+
# ── Typer app ────────────────────────────────────────────────
|
|
40
|
+
app = typer.Typer(
|
|
41
|
+
name="swagger2drawio",
|
|
42
|
+
help="Generate structured Draw.io diagrams from OpenAPI / Swagger specs.",
|
|
43
|
+
add_completion=True,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _version_callback(value: bool) -> None:
|
|
48
|
+
"""Print version and exit when ``--version`` is passed.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
value: ``True`` when the flag is supplied.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
typer.Exit: Always exits after printing.
|
|
55
|
+
"""
|
|
56
|
+
if value:
|
|
57
|
+
typer.echo(f"swagger2drawio {__version__}")
|
|
58
|
+
raise typer.Exit()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.callback()
|
|
62
|
+
def main(
|
|
63
|
+
version: bool | None = typer.Option(
|
|
64
|
+
None,
|
|
65
|
+
"--version",
|
|
66
|
+
help="Show the application version and exit.",
|
|
67
|
+
callback=_version_callback,
|
|
68
|
+
is_eager=True,
|
|
69
|
+
),
|
|
70
|
+
verbose: int = typer.Option(
|
|
71
|
+
0,
|
|
72
|
+
"--verbose",
|
|
73
|
+
"-v",
|
|
74
|
+
count=True,
|
|
75
|
+
help="Increase log verbosity (-v INFO, -vv DEBUG).",
|
|
76
|
+
),
|
|
77
|
+
quiet: bool = typer.Option(
|
|
78
|
+
False,
|
|
79
|
+
"--quiet",
|
|
80
|
+
"-q",
|
|
81
|
+
help="Quiet mode: only WARNING and above.",
|
|
82
|
+
),
|
|
83
|
+
log_file: Path | None = typer.Option(
|
|
84
|
+
None,
|
|
85
|
+
"--log-file",
|
|
86
|
+
help="Append a DEBUG-level log to this file (in addition to console output).",
|
|
87
|
+
),
|
|
88
|
+
) -> None:
|
|
89
|
+
"""swagger2drawio — OpenAPI → Draw.io diagram generator."""
|
|
90
|
+
configure_logging(verbosity=verbose, quiet=quiet, log_file=log_file)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.command()
|
|
94
|
+
def generate(
|
|
95
|
+
input_source: str = typer.Option(
|
|
96
|
+
...,
|
|
97
|
+
"--input",
|
|
98
|
+
"-i",
|
|
99
|
+
help="Path or URL to the OpenAPI/Swagger spec (JSON or YAML).",
|
|
100
|
+
),
|
|
101
|
+
output: Path = typer.Option(
|
|
102
|
+
Path("output.drawio"),
|
|
103
|
+
"--output",
|
|
104
|
+
"-o",
|
|
105
|
+
help="Destination path for the generated .drawio file.",
|
|
106
|
+
),
|
|
107
|
+
config: Path | None = typer.Option(
|
|
108
|
+
None,
|
|
109
|
+
"--config",
|
|
110
|
+
"-c",
|
|
111
|
+
help="Path to an optional YAML theme/config file.",
|
|
112
|
+
),
|
|
113
|
+
theme: str | None = typer.Option(
|
|
114
|
+
None,
|
|
115
|
+
"--theme",
|
|
116
|
+
help="Name of a bundled theme (see `swagger2drawio themes list`).",
|
|
117
|
+
),
|
|
118
|
+
no_validate: bool = typer.Option(
|
|
119
|
+
False,
|
|
120
|
+
"--no-validate",
|
|
121
|
+
help="Skip OpenAPI / Swagger schema validation before rendering.",
|
|
122
|
+
),
|
|
123
|
+
timeout: float = typer.Option(
|
|
124
|
+
10.0,
|
|
125
|
+
"--timeout",
|
|
126
|
+
help="HTTP timeout in seconds when --input is a URL.",
|
|
127
|
+
),
|
|
128
|
+
header: list[str] = typer.Option(
|
|
129
|
+
None,
|
|
130
|
+
"--header",
|
|
131
|
+
"-H",
|
|
132
|
+
help="Extra HTTP header for URL input (repeatable, format: 'Name: value').",
|
|
133
|
+
),
|
|
134
|
+
insecure: bool = typer.Option(
|
|
135
|
+
False,
|
|
136
|
+
"--insecure",
|
|
137
|
+
help="Skip TLS verification (self-signed certs). Use with care.",
|
|
138
|
+
),
|
|
139
|
+
group_by: str = typer.Option(
|
|
140
|
+
"auto",
|
|
141
|
+
"--group-by",
|
|
142
|
+
help="Endpoint grouping: 'tag', 'path', or 'auto' (tag if any tags exist).",
|
|
143
|
+
),
|
|
144
|
+
layout: str = typer.Option(
|
|
145
|
+
"hierarchical",
|
|
146
|
+
"--layout",
|
|
147
|
+
help="Layout strategy for the endpoint graph (e.g. 'hierarchical', 'grid').",
|
|
148
|
+
),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Parse an OpenAPI spec and generate a Draw.io diagram.
|
|
151
|
+
|
|
152
|
+
The command performs the following pipeline:
|
|
153
|
+
|
|
154
|
+
1. **Config** — Loads the theme/layout YAML (falls back to built-in
|
|
155
|
+
defaults when *--config* is not supplied).
|
|
156
|
+
2. **Parse** — Reads the OpenAPI spec from a local file **or** a
|
|
157
|
+
remote URL, extracting paths/methods and component schemas.
|
|
158
|
+
3. **Layout** — Builds in-memory NetworkX graphs and computes (x, y)
|
|
159
|
+
coordinates using the *dot* algorithm.
|
|
160
|
+
4. **Render** — Writes a ``.drawio`` XML file with two pages:
|
|
161
|
+
*Endpoints Map* and *Schema (ER) Map*.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
input_source: A local file path **or** an HTTP(S) URL pointing
|
|
165
|
+
to a valid OpenAPI 3.x / Swagger 2.x specification.
|
|
166
|
+
output: The filesystem path where the ``.drawio`` file will be
|
|
167
|
+
written. Defaults to ``output.drawio`` in the current
|
|
168
|
+
working directory.
|
|
169
|
+
config: Optional path to a YAML file that overrides the default
|
|
170
|
+
visual theme (colours, node sizes, spacing, etc.).
|
|
171
|
+
theme: Optional name of a bundled theme (see
|
|
172
|
+
``swagger2drawio themes list``). Stacked under ``config`` so
|
|
173
|
+
an explicit ``--config`` file can override individual theme
|
|
174
|
+
keys.
|
|
175
|
+
no_validate: When ``True``, skip the OpenAPI / Swagger schema
|
|
176
|
+
validation step. Useful when the spec is known-noncompliant
|
|
177
|
+
but you still want a diagram.
|
|
178
|
+
timeout: HTTP request timeout in seconds when ``input_source``
|
|
179
|
+
is a URL.
|
|
180
|
+
header: Repeatable list of ``"Name: value"`` HTTP headers for
|
|
181
|
+
URL input (e.g. ``--header "Authorization: Bearer ..."``).
|
|
182
|
+
insecure: When ``True``, skip TLS certificate verification.
|
|
183
|
+
group_by: How to group operations on the endpoints page.
|
|
184
|
+
``"tag"`` roots the tree at API tags; ``"path"`` keeps the
|
|
185
|
+
flat structure; ``"auto"`` picks tag when any operation has
|
|
186
|
+
a tag, falling back to path.
|
|
187
|
+
layout: Name of the layout strategy. Built-ins are
|
|
188
|
+
``"hierarchical"`` (the default BFS tree placement) and
|
|
189
|
+
``"grid"`` (one row per weakly-connected component).
|
|
190
|
+
Third-party strategies registered via the entry-point
|
|
191
|
+
group ``swagger2drawio.layouts`` are also discoverable.
|
|
192
|
+
"""
|
|
193
|
+
try:
|
|
194
|
+
if insecure:
|
|
195
|
+
typer.secho(
|
|
196
|
+
"⚠ TLS verification disabled (--insecure). Don't use against untrusted hosts.",
|
|
197
|
+
fg=typer.colors.YELLOW,
|
|
198
|
+
err=True,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
parsed_headers = _parse_headers(header)
|
|
202
|
+
|
|
203
|
+
from rich.progress import (
|
|
204
|
+
BarColumn,
|
|
205
|
+
Progress,
|
|
206
|
+
SpinnerColumn,
|
|
207
|
+
TextColumn,
|
|
208
|
+
TimeElapsedColumn,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
with Progress(
|
|
212
|
+
SpinnerColumn(),
|
|
213
|
+
TextColumn("[bold]{task.description}"),
|
|
214
|
+
BarColumn(bar_width=20),
|
|
215
|
+
TimeElapsedColumn(),
|
|
216
|
+
transient=False,
|
|
217
|
+
) as progress:
|
|
218
|
+
task = progress.add_task("Loading configuration", total=4)
|
|
219
|
+
cfg = load_config(config, theme=theme)
|
|
220
|
+
|
|
221
|
+
progress.update(
|
|
222
|
+
task,
|
|
223
|
+
advance=1,
|
|
224
|
+
description="Parsing & validating spec" if not no_validate else "Parsing spec",
|
|
225
|
+
)
|
|
226
|
+
spec_data = OpenAPIParser(
|
|
227
|
+
source=input_source,
|
|
228
|
+
timeout=timeout,
|
|
229
|
+
headers=parsed_headers,
|
|
230
|
+
verify_ssl=not insecure,
|
|
231
|
+
).parse(validate=not no_validate)
|
|
232
|
+
|
|
233
|
+
progress.update(task, advance=1, description="Computing layout")
|
|
234
|
+
layout_cfg = cfg.get("layout", {})
|
|
235
|
+
nw = layout_cfg.get("node_width", 200)
|
|
236
|
+
nh = layout_cfg.get("node_height", 60)
|
|
237
|
+
hs = layout_cfg.get("horizontal_spacing", 80)
|
|
238
|
+
vs = layout_cfg.get("vertical_spacing", 120)
|
|
239
|
+
rd = layout_cfg.get("rankdir", "TB")
|
|
240
|
+
|
|
241
|
+
ep_layout = build_endpoint_graph(
|
|
242
|
+
spec_data,
|
|
243
|
+
node_width=nw,
|
|
244
|
+
node_height=nh,
|
|
245
|
+
h_spacing=hs,
|
|
246
|
+
v_spacing=vs,
|
|
247
|
+
rankdir=rd,
|
|
248
|
+
group_by=group_by,
|
|
249
|
+
layout=layout,
|
|
250
|
+
)
|
|
251
|
+
sc_layout = build_schema_graph(
|
|
252
|
+
spec_data,
|
|
253
|
+
node_width=nw,
|
|
254
|
+
node_height=nh,
|
|
255
|
+
h_spacing=hs * 1.5,
|
|
256
|
+
v_spacing=vs * 1.3,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
progress.update(task, advance=1, description=f"Rendering → {output.name}")
|
|
260
|
+
render(ep_layout, sc_layout, output, cfg, node_width=nw, node_height=nh)
|
|
261
|
+
progress.update(task, advance=1, description="Done")
|
|
262
|
+
|
|
263
|
+
# ── Final summary (after the live progress bar has closed) ──
|
|
264
|
+
endpoint_count = sum(len(p.methods) for p in spec_data.paths)
|
|
265
|
+
schema_count = len(spec_data.schemas)
|
|
266
|
+
size_bytes = output.stat().st_size if output.exists() else 0
|
|
267
|
+
size_kb = size_bytes / 1024
|
|
268
|
+
typer.secho(
|
|
269
|
+
f"✔ {output} written ({size_kb:.1f} KB) — "
|
|
270
|
+
f"{len(spec_data.paths)} path(s), {endpoint_count} method(s), "
|
|
271
|
+
f"{schema_count} schema(s).",
|
|
272
|
+
fg=typer.colors.GREEN,
|
|
273
|
+
)
|
|
274
|
+
except Swagger2DrawioError as exc:
|
|
275
|
+
_abort(exc)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@app.command()
|
|
279
|
+
def diff(
|
|
280
|
+
old: str = typer.Argument(..., metavar="OLD", help="Path or URL to the old spec."),
|
|
281
|
+
new: str = typer.Argument(..., metavar="NEW", help="Path or URL to the new spec."),
|
|
282
|
+
output: Path = typer.Option(
|
|
283
|
+
Path("changes.drawio"),
|
|
284
|
+
"--output",
|
|
285
|
+
"-o",
|
|
286
|
+
help="Destination .drawio file for the rendered diff.",
|
|
287
|
+
),
|
|
288
|
+
) -> None:
|
|
289
|
+
"""Compare two specs and emit a colour-coded ``.drawio`` change report.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
old: Path or URL to the earlier spec.
|
|
293
|
+
new: Path or URL to the later spec.
|
|
294
|
+
output: Destination ``.drawio`` file.
|
|
295
|
+
"""
|
|
296
|
+
from swagger2drawio.diff import compute_diff
|
|
297
|
+
from swagger2drawio.renderer.diff_renderer import render_diff
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
typer.echo(f"⏳ Parsing OLD spec: {old}")
|
|
301
|
+
old_parsed = OpenAPIParser(source=old).parse(validate=False)
|
|
302
|
+
typer.echo(f"⏳ Parsing NEW spec: {new}")
|
|
303
|
+
new_parsed = OpenAPIParser(source=new).parse(validate=False)
|
|
304
|
+
|
|
305
|
+
typer.echo("⏳ Computing diff …")
|
|
306
|
+
result = compute_diff(old_parsed, new_parsed)
|
|
307
|
+
|
|
308
|
+
if result.is_empty:
|
|
309
|
+
typer.secho("✔ No structural changes detected.", fg=typer.colors.GREEN)
|
|
310
|
+
else:
|
|
311
|
+
typer.echo(f" paths +{len(result.paths_added)} -{len(result.paths_removed)}")
|
|
312
|
+
typer.echo(
|
|
313
|
+
f" methods +{len(result.methods_added)} -{len(result.methods_removed)} "
|
|
314
|
+
f"~{len(result.methods_changed)}"
|
|
315
|
+
)
|
|
316
|
+
typer.echo(
|
|
317
|
+
f" schemas +{len(result.schemas_added)} -{len(result.schemas_removed)} "
|
|
318
|
+
f"~{len(result.schemas_changed)}"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
typer.echo(f"⏳ Rendering diff → {output}")
|
|
322
|
+
render_diff(old_parsed, new_parsed, result, output)
|
|
323
|
+
typer.secho(f"✔ Diff diagram saved to {output}", fg=typer.colors.GREEN)
|
|
324
|
+
except Swagger2DrawioError as exc:
|
|
325
|
+
_abort(exc)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@app.command()
|
|
329
|
+
def validate(
|
|
330
|
+
input_source: str = typer.Argument(
|
|
331
|
+
...,
|
|
332
|
+
metavar="SPEC",
|
|
333
|
+
help="Path or URL to the OpenAPI/Swagger spec to validate.",
|
|
334
|
+
),
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Validate an OpenAPI / Swagger spec without rendering anything.
|
|
337
|
+
|
|
338
|
+
Exits 0 when valid; otherwise prints the validator's message and
|
|
339
|
+
exits with the SpecValidationError exit code so CI scripts can react.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
input_source: Local file path or HTTP(S) URL to the spec.
|
|
343
|
+
"""
|
|
344
|
+
from swagger2drawio.parser.openapi_parser import _load_raw_spec
|
|
345
|
+
from swagger2drawio.validator import validate_spec
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
typer.echo(f"⏳ Validating: {input_source}")
|
|
349
|
+
raw = _load_raw_spec(input_source)
|
|
350
|
+
validate_spec(raw)
|
|
351
|
+
except Swagger2DrawioError as exc:
|
|
352
|
+
_abort(exc)
|
|
353
|
+
else:
|
|
354
|
+
typer.secho("✔ Spec is valid.", fg=typer.colors.GREEN)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# ── themes sub-app ───────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
themes_app = typer.Typer(name="themes", help="List or print bundled themes.", no_args_is_help=True)
|
|
361
|
+
app.add_typer(themes_app, name="themes")
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@themes_app.command("list")
|
|
365
|
+
def themes_list() -> None:
|
|
366
|
+
"""Print every bundled theme name, one per line."""
|
|
367
|
+
for name in list_builtin_themes():
|
|
368
|
+
typer.echo(name)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@themes_app.command("show")
|
|
372
|
+
def themes_show(name: str = typer.Argument(..., help="Theme name (e.g. 'dark').")) -> None:
|
|
373
|
+
"""Print the resolved YAML of one bundled theme.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
name: Theme name (without ``.yaml``).
|
|
377
|
+
"""
|
|
378
|
+
try:
|
|
379
|
+
data = read_theme(name)
|
|
380
|
+
except Swagger2DrawioError as exc:
|
|
381
|
+
_abort(exc)
|
|
382
|
+
import yaml as _yaml
|
|
383
|
+
|
|
384
|
+
typer.echo(_yaml.safe_dump(data, sort_keys=False))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
# ── init subcommand ──────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@app.command()
|
|
391
|
+
def init(
|
|
392
|
+
theme: str = typer.Option(
|
|
393
|
+
"light",
|
|
394
|
+
"--theme",
|
|
395
|
+
help="Bundled theme to scaffold from (light / dark / pastel / high-contrast).",
|
|
396
|
+
),
|
|
397
|
+
force: bool = typer.Option(False, "--force", help="Overwrite an existing config file."),
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Scaffold a ``.swagger2drawio.yaml`` in the current directory.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
theme: Name of the bundled theme to copy as the starter file.
|
|
403
|
+
force: Overwrite an existing file instead of refusing.
|
|
404
|
+
"""
|
|
405
|
+
import yaml as _yaml
|
|
406
|
+
|
|
407
|
+
target = Path.cwd() / ".swagger2drawio.yaml"
|
|
408
|
+
if target.exists() and not force:
|
|
409
|
+
typer.secho(
|
|
410
|
+
f"✖ {target.name} already exists; pass --force to overwrite.",
|
|
411
|
+
fg=typer.colors.RED,
|
|
412
|
+
err=True,
|
|
413
|
+
)
|
|
414
|
+
raise typer.Exit(code=1)
|
|
415
|
+
try:
|
|
416
|
+
data = read_theme(theme)
|
|
417
|
+
except Swagger2DrawioError as exc:
|
|
418
|
+
_abort(exc)
|
|
419
|
+
target.write_text(_yaml.safe_dump(data, sort_keys=False), encoding="utf-8")
|
|
420
|
+
typer.secho(f"✔ Wrote {target}", fg=typer.colors.GREEN)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _parse_headers(raw: list[str] | None) -> dict[str, str] | None:
|
|
424
|
+
"""Convert a list of ``"Name: value"`` strings into a header dict.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
raw: List of strings as collected from ``--header / -H`` (may be
|
|
428
|
+
``None`` if the flag was not passed).
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
``None`` when no headers were supplied; otherwise a dict mapping
|
|
432
|
+
header name to value.
|
|
433
|
+
|
|
434
|
+
Raises:
|
|
435
|
+
typer.BadParameter: If any entry is missing the ``:`` separator.
|
|
436
|
+
"""
|
|
437
|
+
if not raw:
|
|
438
|
+
return None
|
|
439
|
+
parsed: dict[str, str] = {}
|
|
440
|
+
for entry in raw:
|
|
441
|
+
if ":" not in entry:
|
|
442
|
+
raise typer.BadParameter(f"Invalid --header value: {entry!r}. Expected 'Name: value'.")
|
|
443
|
+
name, _, value = entry.partition(":")
|
|
444
|
+
parsed[name.strip()] = value.strip()
|
|
445
|
+
return parsed
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _abort(exc: Swagger2DrawioError) -> None:
|
|
449
|
+
"""Print a clean error and exit with the code mapped to *exc*'s class.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
exc: The typed exception to report.
|
|
453
|
+
|
|
454
|
+
Raises:
|
|
455
|
+
typer.Exit: Always raised with the appropriate code.
|
|
456
|
+
"""
|
|
457
|
+
code = EXIT_CODE.get(type(exc), EXIT_CODE[Swagger2DrawioError])
|
|
458
|
+
typer.secho(f"✖ {type(exc).__name__}: {exc}", fg=typer.colors.RED, err=True)
|
|
459
|
+
raise typer.Exit(code=code) from exc
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# Allow ``python -m swagger2drawio.cli`` execution.
|
|
463
|
+
if __name__ == "__main__":
|
|
464
|
+
app()
|