cli-wizard 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.
- cli_wizard/__init__.py +4 -0
- cli_wizard/cli.py +37 -0
- cli_wizard/commands/__init__.py +4 -0
- cli_wizard/commands/common.py +36 -0
- cli_wizard/commands/config.py +82 -0
- cli_wizard/commands/generate.py +231 -0
- cli_wizard/config/__init__.py +0 -0
- cli_wizard/config/configuration.py +53 -0
- cli_wizard/config/default_config.yaml +7 -0
- cli_wizard/config/schema.py +182 -0
- cli_wizard/constants.py +11 -0
- cli_wizard/generator/__init__.py +22 -0
- cli_wizard/generator/generator.py +241 -0
- cli_wizard/generator/models.py +123 -0
- cli_wizard/generator/parser.py +171 -0
- cli_wizard/templates/README.md.j2 +31 -0
- cli_wizard/templates/cli.py.j2 +85 -0
- cli_wizard/templates/client.py.j2 +134 -0
- cli_wizard/templates/command_group.py.j2 +161 -0
- cli_wizard/templates/commands_config.py.j2 +206 -0
- cli_wizard/templates/commands_init.py.j2 +14 -0
- cli_wizard/templates/constants.py.j2 +67 -0
- cli_wizard/templates/logging.py.j2 +175 -0
- cli_wizard/templates/package_init.py.j2 +8 -0
- cli_wizard/templates/profile.py.j2 +87 -0
- cli_wizard/templates/pyproject.toml.j2 +28 -0
- cli_wizard-1.0.0.dist-info/METADATA +236 -0
- cli_wizard-1.0.0.dist-info/RECORD +32 -0
- cli_wizard-1.0.0.dist-info/WHEEL +5 -0
- cli_wizard-1.0.0.dist-info/entry_points.txt +2 -0
- cli_wizard-1.0.0.dist-info/licenses/LICENSE +21 -0
- cli_wizard-1.0.0.dist-info/top_level.txt +1 -0
cli_wizard/__init__.py
ADDED
cli_wizard/cli.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright (c) 2026, Giacomo Marciani
|
|
2
|
+
# Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
"""Main CLI module for CLI Wizard."""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import logging
|
|
8
|
+
from cli_wizard.constants import __version__
|
|
9
|
+
from cli_wizard.commands.config import config
|
|
10
|
+
from cli_wizard.commands.generate import generate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.group()
|
|
14
|
+
@click.version_option(version=__version__, prog_name="cli-wizard")
|
|
15
|
+
@click.option("--debug", "-d", is_flag=True, help="Enable debug output")
|
|
16
|
+
@click.pass_context
|
|
17
|
+
def main(ctx: click.Context, debug: bool) -> None:
|
|
18
|
+
"""CLI Wizard - Generate modern CLI from OpenAPI."""
|
|
19
|
+
ctx.ensure_object(dict)
|
|
20
|
+
ctx.obj["debug"] = debug
|
|
21
|
+
|
|
22
|
+
# Configure logging
|
|
23
|
+
level = logging.DEBUG if debug else logging.INFO
|
|
24
|
+
logging.basicConfig(
|
|
25
|
+
level=level,
|
|
26
|
+
format="%(asctime)s %(levelname)s: %(message)s",
|
|
27
|
+
datefmt="%Y-%m-%dT%H:%M:%SZ",
|
|
28
|
+
)
|
|
29
|
+
logging.Formatter.converter = lambda *args: __import__("time").gmtime()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
main.add_command(config)
|
|
33
|
+
main.add_command(generate)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright (c) 2026, Giacomo Marciani
|
|
2
|
+
# Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
"""Common utilities for CLI Wizard commands."""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def configure_logging(debug: bool = False) -> None:
|
|
13
|
+
"""Configure logging with UTC timestamps.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
debug: Whether to enable debug level logging
|
|
17
|
+
"""
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
for handler in logging.root.handlers[:]:
|
|
21
|
+
logging.root.removeHandler(handler)
|
|
22
|
+
level = logging.DEBUG if debug else logging.INFO
|
|
23
|
+
logging.basicConfig(
|
|
24
|
+
level=level,
|
|
25
|
+
format="%(asctime)s %(levelname)s: %(message)s",
|
|
26
|
+
datefmt="%Y-%m-%dT%H:%M:%SZ",
|
|
27
|
+
)
|
|
28
|
+
logging.Formatter.converter = time.gmtime
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
debug_option = click.option(
|
|
32
|
+
"--debug",
|
|
33
|
+
"-d",
|
|
34
|
+
is_flag=True,
|
|
35
|
+
help="Enable debug output",
|
|
36
|
+
)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright (c) 2026, Giacomo Marciani
|
|
2
|
+
# Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
"""Configuration commands for CLI Wizard."""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import logging
|
|
8
|
+
import json
|
|
9
|
+
from cli_wizard.config.configuration import (
|
|
10
|
+
load_config,
|
|
11
|
+
save_config,
|
|
12
|
+
get_config_path,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@click.group()
|
|
19
|
+
def config() -> None:
|
|
20
|
+
"""Manage configurations."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@config.command()
|
|
24
|
+
@click.argument("key")
|
|
25
|
+
@click.argument("value")
|
|
26
|
+
def set(key: str, value: str) -> None:
|
|
27
|
+
"""Set a configuration value."""
|
|
28
|
+
cfg = load_config()
|
|
29
|
+
old_value = cfg.get(key)
|
|
30
|
+
cfg[key] = value
|
|
31
|
+
save_config(cfg)
|
|
32
|
+
|
|
33
|
+
result = {"key": key, "value": value, "oldValue": old_value}
|
|
34
|
+
print(json.dumps(result, indent=2))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@config.command()
|
|
38
|
+
@click.argument("key")
|
|
39
|
+
def get(key: str) -> None:
|
|
40
|
+
"""Get a configuration value."""
|
|
41
|
+
cfg = load_config()
|
|
42
|
+
if key not in cfg:
|
|
43
|
+
logger.error(f"Unknown configuration key '{key}'")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
result = {"key": key, "value": cfg[key]}
|
|
47
|
+
print(json.dumps(result, indent=2))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@config.command()
|
|
51
|
+
@click.argument("key")
|
|
52
|
+
def unset(key: str) -> None:
|
|
53
|
+
"""Unset a configuration value (set to None)."""
|
|
54
|
+
cfg = load_config()
|
|
55
|
+
if key not in cfg:
|
|
56
|
+
logger.error(f"Unknown configuration key '{key}'")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
old_value = cfg.get(key)
|
|
60
|
+
cfg[key] = None
|
|
61
|
+
save_config(cfg)
|
|
62
|
+
|
|
63
|
+
result = {"key": key, "value": None, "oldValue": old_value}
|
|
64
|
+
print(json.dumps(result, indent=2))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@config.command()
|
|
68
|
+
def show() -> None:
|
|
69
|
+
"""Show all configuration values as JSON."""
|
|
70
|
+
cfg = load_config()
|
|
71
|
+
print(json.dumps(cfg, indent=2))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@config.command()
|
|
75
|
+
def reset() -> None:
|
|
76
|
+
"""Reset configuration to defaults and delete local config file."""
|
|
77
|
+
cfg = load_config()
|
|
78
|
+
print(json.dumps(cfg, indent=2))
|
|
79
|
+
|
|
80
|
+
config_path = get_config_path()
|
|
81
|
+
if config_path.exists():
|
|
82
|
+
config_path.unlink()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Copyright (c) 2026, Giacomo Marciani
|
|
2
|
+
# Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
"""Generate command for CLI Wizard."""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
import logging
|
|
11
|
+
import shutil
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
from pydantic import ValidationError
|
|
16
|
+
|
|
17
|
+
from cli_wizard.config.configuration import load_default_config
|
|
18
|
+
from cli_wizard.config.schema import Config
|
|
19
|
+
from cli_wizard.generator import OpenApiParser, CliGenerator
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Load defaults from config
|
|
24
|
+
_defaults = load_default_config()
|
|
25
|
+
_default_openapi = _defaults.get("OpenApiFileName", "openapi.yaml")
|
|
26
|
+
_default_config = _defaults.get("ConfigFileName", "config.yaml")
|
|
27
|
+
_default_output = _defaults.get("OutputDir", "cli")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@click.command()
|
|
31
|
+
@click.option(
|
|
32
|
+
"--working-dir",
|
|
33
|
+
"-w",
|
|
34
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
35
|
+
default=None,
|
|
36
|
+
help="Working directory for resolving relative paths",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--openapi",
|
|
40
|
+
"-o",
|
|
41
|
+
type=click.Path(dir_okay=False),
|
|
42
|
+
default=_default_openapi,
|
|
43
|
+
help=f"Path to OpenAPI spec file in YAML or JSON format (default: {_default_openapi})",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--config",
|
|
47
|
+
"-c",
|
|
48
|
+
type=click.Path(dir_okay=False),
|
|
49
|
+
default=_default_config,
|
|
50
|
+
help=f"Path to config YAML file (default: {_default_config})",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--output",
|
|
54
|
+
"-d",
|
|
55
|
+
type=click.Path(file_okay=False),
|
|
56
|
+
default=_default_output,
|
|
57
|
+
help=f"Output directory for generated CLI (default: {_default_output})",
|
|
58
|
+
)
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def generate(
|
|
61
|
+
ctx: click.Context,
|
|
62
|
+
working_dir: str | None,
|
|
63
|
+
openapi: str,
|
|
64
|
+
config: str,
|
|
65
|
+
output: str,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Generate a CLI from an OpenAPI spec and config file."""
|
|
68
|
+
debug = ctx.obj.get("debug", False) if ctx.obj else False
|
|
69
|
+
|
|
70
|
+
# Resolve paths relative to working directory
|
|
71
|
+
base_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
72
|
+
openapi_path = (
|
|
73
|
+
base_dir / openapi if not Path(openapi).is_absolute() else Path(openapi)
|
|
74
|
+
)
|
|
75
|
+
config_path = base_dir / config if not Path(config).is_absolute() else Path(config)
|
|
76
|
+
|
|
77
|
+
if debug:
|
|
78
|
+
logger.debug(f"Working directory: {base_dir}")
|
|
79
|
+
logger.debug(f"OpenAPI spec: {openapi_path}")
|
|
80
|
+
logger.debug(f"Config file: {config_path}")
|
|
81
|
+
|
|
82
|
+
# Validate input files exist
|
|
83
|
+
if not openapi_path.exists():
|
|
84
|
+
click.secho(
|
|
85
|
+
f"✗ OpenAPI spec file not found: {openapi_path}", fg="red", err=True
|
|
86
|
+
)
|
|
87
|
+
raise SystemExit(1)
|
|
88
|
+
if not config_path.exists():
|
|
89
|
+
click.secho(f"✗ Config file not found: {config_path}", fg="red", err=True)
|
|
90
|
+
raise SystemExit(1)
|
|
91
|
+
|
|
92
|
+
# Load and validate configuration
|
|
93
|
+
cli_config = _load_cli_config(config_path)
|
|
94
|
+
|
|
95
|
+
# Resolve output path (CLI option > config > default)
|
|
96
|
+
output_dir: str = output
|
|
97
|
+
if output == _default_output:
|
|
98
|
+
config_output = cli_config.get("OutputDir")
|
|
99
|
+
if config_output is not None:
|
|
100
|
+
output_dir = str(config_output)
|
|
101
|
+
output_path = (
|
|
102
|
+
base_dir / output_dir
|
|
103
|
+
if not Path(output_dir).is_absolute()
|
|
104
|
+
else Path(output_dir)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if debug:
|
|
108
|
+
logger.debug(f"Output directory: {output_path}")
|
|
109
|
+
|
|
110
|
+
# Get CLI name and package name from config
|
|
111
|
+
cli_name = cli_config["PackageName"]
|
|
112
|
+
# Convert hyphens to underscores for Python package compatibility
|
|
113
|
+
package_name = cli_name.replace("-", "_")
|
|
114
|
+
|
|
115
|
+
# Parse OpenAPI spec
|
|
116
|
+
click.secho("📄 Parsing OpenAPI spec: ", fg="cyan", nl=False)
|
|
117
|
+
click.echo(openapi_path)
|
|
118
|
+
parser = OpenApiParser(str(openapi_path))
|
|
119
|
+
|
|
120
|
+
groups = parser.parse(
|
|
121
|
+
exclude_tags=cli_config.get("ExcludeTags", []),
|
|
122
|
+
include_tags=cli_config.get("IncludeTags", []),
|
|
123
|
+
tag_mapping=cli_config.get("TagMapping", {}),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not groups:
|
|
127
|
+
click.secho("✗ No operations found in OpenAPI spec", fg="red", err=True)
|
|
128
|
+
raise SystemExit(1)
|
|
129
|
+
|
|
130
|
+
# Clean up output directory before generating
|
|
131
|
+
if output_path.exists():
|
|
132
|
+
# Check if we're inside the output directory
|
|
133
|
+
try:
|
|
134
|
+
cwd = Path.cwd()
|
|
135
|
+
if output_path in cwd.parents or output_path == cwd:
|
|
136
|
+
click.secho(
|
|
137
|
+
f"✗ Cannot clean output directory while inside it. "
|
|
138
|
+
f"Please run from a different directory.",
|
|
139
|
+
fg="red",
|
|
140
|
+
err=True,
|
|
141
|
+
)
|
|
142
|
+
raise SystemExit(1)
|
|
143
|
+
except OSError:
|
|
144
|
+
# Current directory may already be deleted
|
|
145
|
+
pass
|
|
146
|
+
click.secho("🧹 Cleaning output directory: ", fg="cyan", nl=False)
|
|
147
|
+
click.echo(output_path)
|
|
148
|
+
shutil.rmtree(output_path)
|
|
149
|
+
|
|
150
|
+
# Generate CLI project
|
|
151
|
+
click.secho("⚙️ Generating CLI project: ", fg="cyan", nl=False)
|
|
152
|
+
click.echo(output_path)
|
|
153
|
+
generator = CliGenerator(config=cli_config, config_dir=config_path.parent)
|
|
154
|
+
generator.generate(groups, output_path, cli_name, package_name)
|
|
155
|
+
|
|
156
|
+
# Summary
|
|
157
|
+
click.secho(f"\n✓ Generated CLI '{cli_name}'", fg="green", bold=True)
|
|
158
|
+
click.secho(" 📁 Location: ", fg="white", nl=False)
|
|
159
|
+
click.echo(output_path)
|
|
160
|
+
click.secho(" 📦 Package: ", fg="white", nl=False)
|
|
161
|
+
click.echo(package_name)
|
|
162
|
+
click.secho(" 🔧 Commands: ", fg="white", nl=False)
|
|
163
|
+
click.echo(f"{len(groups)} groups")
|
|
164
|
+
for tag, group in groups.items():
|
|
165
|
+
click.secho(f" • {group.cli_name}", fg="yellow", nl=False)
|
|
166
|
+
click.echo(f" ({len(group.operations)} commands)")
|
|
167
|
+
|
|
168
|
+
click.echo()
|
|
169
|
+
click.secho("📋 Next steps:", fg="cyan", bold=True)
|
|
170
|
+
click.echo(f" pip install -e {output_path}")
|
|
171
|
+
click.echo(f" {cli_name} --help")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _load_cli_config(config_path: Path) -> dict:
|
|
175
|
+
"""Load and validate CLI generator configuration from YAML file."""
|
|
176
|
+
try:
|
|
177
|
+
with open(config_path) as f:
|
|
178
|
+
raw_config = yaml.safe_load(f) or {}
|
|
179
|
+
except (yaml.YAMLError, IOError) as e:
|
|
180
|
+
click.secho(f"✗ Could not load config file: {e}", fg="red", err=True)
|
|
181
|
+
raise SystemExit(1)
|
|
182
|
+
|
|
183
|
+
# Validate with Pydantic schema
|
|
184
|
+
try:
|
|
185
|
+
validated = Config(**raw_config)
|
|
186
|
+
config = validated.model_dump()
|
|
187
|
+
except ValidationError as e:
|
|
188
|
+
click.secho("✗ Invalid configuration:", fg="red", err=True)
|
|
189
|
+
for error in e.errors():
|
|
190
|
+
field = ".".join(str(loc) for loc in error["loc"])
|
|
191
|
+
click.secho(f" • {field}: {error['msg']}", fg="red", err=True)
|
|
192
|
+
raise SystemExit(1)
|
|
193
|
+
|
|
194
|
+
# Expand #[Param] references
|
|
195
|
+
return _expand_config_references(config)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _expand_config_references(config: dict[str, Any]) -> dict[str, Any]:
|
|
199
|
+
"""Expand #[Param] references in config values recursively.
|
|
200
|
+
|
|
201
|
+
Supports referencing other config parameters using #[ParamName] syntax.
|
|
202
|
+
Environment variables using ${VAR} syntax are left as-is for runtime expansion.
|
|
203
|
+
Recursively expands until no more references remain.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
def expand_value(value: Any, config: dict[str, Any]) -> Any:
|
|
207
|
+
if isinstance(value, str):
|
|
208
|
+
# Keep expanding until no more #[Param] references
|
|
209
|
+
pattern = r"#\[(\w+)\]"
|
|
210
|
+
prev_value: str | None = None
|
|
211
|
+
while prev_value != value:
|
|
212
|
+
prev_value = value
|
|
213
|
+
matches = re.findall(pattern, value)
|
|
214
|
+
for param_name in matches:
|
|
215
|
+
if param_name in config:
|
|
216
|
+
replacement = config[param_name]
|
|
217
|
+
if isinstance(replacement, str):
|
|
218
|
+
value = value.replace(f"#[{param_name}]", replacement)
|
|
219
|
+
return value
|
|
220
|
+
elif isinstance(value, dict):
|
|
221
|
+
return {k: expand_value(v, config) for k, v in value.items()}
|
|
222
|
+
elif isinstance(value, list):
|
|
223
|
+
return [expand_value(item, config) for item in value]
|
|
224
|
+
return value
|
|
225
|
+
|
|
226
|
+
# First pass: expand all values
|
|
227
|
+
expanded: dict[str, Any] = expand_value(config, config)
|
|
228
|
+
|
|
229
|
+
# Second pass: re-expand with updated config to handle nested references
|
|
230
|
+
result: dict[str, Any] = expand_value(expanded, expanded)
|
|
231
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Copyright (c) 2026, Giacomo Marciani
|
|
2
|
+
# Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
"""Configuration utilities for CLI Wizard."""
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Any
|
|
9
|
+
import importlib.resources
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_config_path() -> Path:
|
|
13
|
+
"""Get the configuration file path."""
|
|
14
|
+
config_dir = Path.home() / ".cli_wizard"
|
|
15
|
+
config_dir.mkdir(exist_ok=True)
|
|
16
|
+
return config_dir / "config.yaml"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_default_config() -> Dict[str, Any]:
|
|
20
|
+
"""Load default configuration from packaged default_config.yaml."""
|
|
21
|
+
try:
|
|
22
|
+
with importlib.resources.open_text(
|
|
23
|
+
"cli_wizard.config", "default_config.yaml"
|
|
24
|
+
) as f:
|
|
25
|
+
data = yaml.safe_load(f)
|
|
26
|
+
return data if isinstance(data, dict) else {}
|
|
27
|
+
except (yaml.YAMLError, IOError, FileNotFoundError):
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_config() -> Dict[str, Any]:
|
|
32
|
+
"""Load configuration from file, merging with defaults."""
|
|
33
|
+
default_config = load_default_config()
|
|
34
|
+
config_path = get_config_path()
|
|
35
|
+
|
|
36
|
+
if not config_path.exists():
|
|
37
|
+
return default_config
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
with open(config_path, "r") as f:
|
|
41
|
+
user_config = yaml.safe_load(f)
|
|
42
|
+
if isinstance(user_config, dict):
|
|
43
|
+
default_config.update(user_config)
|
|
44
|
+
return default_config
|
|
45
|
+
except (yaml.YAMLError, IOError):
|
|
46
|
+
return default_config
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def save_config(config: Dict[str, Any]) -> None:
|
|
50
|
+
"""Save configuration to file."""
|
|
51
|
+
config_path = get_config_path()
|
|
52
|
+
with open(config_path, "w") as f:
|
|
53
|
+
yaml.safe_dump(config, f, indent=2)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ConfigFileName: config.yaml
|
|
2
|
+
OpenApiFileName: openapi.json
|
|
3
|
+
OutputDir: cli
|
|
4
|
+
# PackageName sets the Python package name for the generated CLI.
|
|
5
|
+
# Hyphens are automatically converted to underscores for Python compatibility.
|
|
6
|
+
# Example: "my-cli" becomes "my_cli" for the package folder.
|
|
7
|
+
PackageName: my-cli
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Copyright (c) 2026, Giacomo Marciani
|
|
2
|
+
# Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
"""Configuration schema for CLI Wizard."""
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field, field_validator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Config(BaseModel):
|
|
13
|
+
"""CLI Wizard configuration schema."""
|
|
14
|
+
|
|
15
|
+
# Required parameters
|
|
16
|
+
PackageName: str = Field(
|
|
17
|
+
..., description="Python package name for the generated CLI"
|
|
18
|
+
)
|
|
19
|
+
DefaultBaseUrl: str = Field(..., description="Default API base URL")
|
|
20
|
+
|
|
21
|
+
# Output settings
|
|
22
|
+
OutputDir: str = Field(
|
|
23
|
+
default="#[PackageName]",
|
|
24
|
+
description="Output directory for the generated CLI project",
|
|
25
|
+
)
|
|
26
|
+
MainDir: str = Field(
|
|
27
|
+
default="${HOME}/.#[PackageName]",
|
|
28
|
+
description="Main directory for CLI data (config, cache, logging, etc.)",
|
|
29
|
+
)
|
|
30
|
+
ProfileFile: str = Field(
|
|
31
|
+
default="#[MainDir]/profiles.yaml",
|
|
32
|
+
description="Path to profiles YAML file",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# OpenAPI settings
|
|
36
|
+
OpenapiSpec: str = Field(
|
|
37
|
+
default="openapi.json",
|
|
38
|
+
description="Path to OpenAPI spec (relative to config file or absolute)",
|
|
39
|
+
)
|
|
40
|
+
ExcludeTags: list[str] = Field(
|
|
41
|
+
default_factory=list,
|
|
42
|
+
description="Tags to exclude from generation",
|
|
43
|
+
)
|
|
44
|
+
IncludeTags: list[str] = Field(
|
|
45
|
+
default_factory=list,
|
|
46
|
+
description="Tags to include (if empty, all non-excluded tags are included)",
|
|
47
|
+
)
|
|
48
|
+
TagMapping: dict[str, str] = Field(
|
|
49
|
+
default_factory=dict,
|
|
50
|
+
description="Map OpenAPI tags to CLI command group names",
|
|
51
|
+
)
|
|
52
|
+
CommandMapping: dict[str, str] = Field(
|
|
53
|
+
default_factory=dict,
|
|
54
|
+
description="Customize command names (operationId -> command name)",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Output formatting
|
|
58
|
+
OutputFormat: Literal["json", "table", "yaml"] = Field(
|
|
59
|
+
default="json",
|
|
60
|
+
description="Default output format",
|
|
61
|
+
)
|
|
62
|
+
OutputColors: bool = Field(
|
|
63
|
+
default=True,
|
|
64
|
+
description="Enable colored output",
|
|
65
|
+
)
|
|
66
|
+
JsonIndent: int = Field(
|
|
67
|
+
default=2,
|
|
68
|
+
ge=0,
|
|
69
|
+
description="JSON indentation",
|
|
70
|
+
)
|
|
71
|
+
TableStyle: Literal["ascii", "rounded", "minimal", "markdown"] = Field(
|
|
72
|
+
default="rounded",
|
|
73
|
+
description="Table style",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Splash screen
|
|
77
|
+
SplashFile: str | None = Field(
|
|
78
|
+
default=None,
|
|
79
|
+
description="Path to splash text file (relative to config or absolute)",
|
|
80
|
+
)
|
|
81
|
+
SplashColor: str = Field(
|
|
82
|
+
default="#FFFFFF",
|
|
83
|
+
description="Color for splash text (hex code)",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Logging
|
|
87
|
+
LogLevel: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = Field(
|
|
88
|
+
default="INFO",
|
|
89
|
+
description="Default log level",
|
|
90
|
+
)
|
|
91
|
+
LogFormat: str = Field(
|
|
92
|
+
default="[%(levelname)s] %(asctime)s %(message)s",
|
|
93
|
+
description="Log message format (Python logging format)",
|
|
94
|
+
)
|
|
95
|
+
LogTimestampFormat: str = Field(
|
|
96
|
+
default="%Y-%m-%dT%H:%M:%S",
|
|
97
|
+
description="Timestamp format for log messages (strftime format)",
|
|
98
|
+
)
|
|
99
|
+
LogTimezone: Literal["UTC", "Local"] = Field(
|
|
100
|
+
default="UTC",
|
|
101
|
+
description="Timezone for log timestamps",
|
|
102
|
+
)
|
|
103
|
+
LogColorStyle: Literal["full", "level"] = Field(
|
|
104
|
+
default="level",
|
|
105
|
+
description="Log color style: 'full' colors entire line, 'level' colors only the level prefix",
|
|
106
|
+
)
|
|
107
|
+
LogColorDebug: str = Field(
|
|
108
|
+
default="#808080",
|
|
109
|
+
description="Color for DEBUG log level (hex code)",
|
|
110
|
+
)
|
|
111
|
+
LogColorInfo: str = Field(
|
|
112
|
+
default="#00FF00",
|
|
113
|
+
description="Color for INFO log level (hex code)",
|
|
114
|
+
)
|
|
115
|
+
LogColorWarning: str = Field(
|
|
116
|
+
default="#FFFF00",
|
|
117
|
+
description="Color for WARNING log level (hex code)",
|
|
118
|
+
)
|
|
119
|
+
LogColorError: str = Field(
|
|
120
|
+
default="#FF0000",
|
|
121
|
+
description="Color for ERROR log level (hex code)",
|
|
122
|
+
)
|
|
123
|
+
LogFile: str | None = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="Path to log file (None means no file logging)",
|
|
126
|
+
)
|
|
127
|
+
LogRotationType: Literal["size", "days"] = Field(
|
|
128
|
+
default="days",
|
|
129
|
+
description="Log rotation type: 'size' for file size, 'days' for time-based",
|
|
130
|
+
)
|
|
131
|
+
LogRotationSize: int = Field(
|
|
132
|
+
default=10,
|
|
133
|
+
ge=1,
|
|
134
|
+
description="Log rotation size in MB (when LogRotationType is 'size')",
|
|
135
|
+
)
|
|
136
|
+
LogRotationDays: int = Field(
|
|
137
|
+
default=30,
|
|
138
|
+
ge=1,
|
|
139
|
+
description="Log rotation interval in days (when LogRotationType is 'days')",
|
|
140
|
+
)
|
|
141
|
+
LogRotationBackupCount: int = Field(
|
|
142
|
+
default=5,
|
|
143
|
+
ge=0,
|
|
144
|
+
description="Number of backup log files to keep",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# API client settings
|
|
148
|
+
Timeout: int = Field(
|
|
149
|
+
default=30,
|
|
150
|
+
ge=1,
|
|
151
|
+
description="Request timeout in seconds",
|
|
152
|
+
)
|
|
153
|
+
CaFile: str | None = Field(
|
|
154
|
+
default=None,
|
|
155
|
+
description="CA certificate file for SSL verification (relative to config or absolute)",
|
|
156
|
+
)
|
|
157
|
+
RetryMaxAttempts: int = Field(
|
|
158
|
+
default=3,
|
|
159
|
+
ge=0,
|
|
160
|
+
description="Retry max attempts",
|
|
161
|
+
)
|
|
162
|
+
RetryBackoffFactor: float = Field(
|
|
163
|
+
default=0.5,
|
|
164
|
+
ge=0,
|
|
165
|
+
description="Retry backoff factor",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@field_validator(
|
|
169
|
+
"SplashColor",
|
|
170
|
+
"LogColorDebug",
|
|
171
|
+
"LogColorInfo",
|
|
172
|
+
"LogColorWarning",
|
|
173
|
+
"LogColorError",
|
|
174
|
+
)
|
|
175
|
+
@classmethod
|
|
176
|
+
def validate_hex_color(cls, v: str) -> str:
|
|
177
|
+
"""Validate that color is a valid hex color code."""
|
|
178
|
+
if not re.match(r"^#[0-9A-Fa-f]{6}$", v):
|
|
179
|
+
raise ValueError(f"Invalid hex color code: {v}. Must be in format #RRGGBB")
|
|
180
|
+
return v.upper()
|
|
181
|
+
|
|
182
|
+
model_config = {"extra": "forbid"}
|
cli_wizard/constants.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copyright (c) 2026, Giacomo Marciani
|
|
2
|
+
# Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
"""Constants for CLI Wizard."""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("cli-wizard")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "0.0.0"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Copyright (c) 2026, Giacomo Marciani
|
|
2
|
+
# Licensed under the MIT License
|
|
3
|
+
|
|
4
|
+
"""Generator package for CLI Wizard."""
|
|
5
|
+
|
|
6
|
+
from cli_wizard.generator.parser import OpenApiParser
|
|
7
|
+
from cli_wizard.generator.generator import CliGenerator
|
|
8
|
+
from cli_wizard.generator.models import (
|
|
9
|
+
Parameter,
|
|
10
|
+
RequestBodyProperty,
|
|
11
|
+
Operation,
|
|
12
|
+
CommandGroup,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"OpenApiParser",
|
|
17
|
+
"CliGenerator",
|
|
18
|
+
"Parameter",
|
|
19
|
+
"RequestBodyProperty",
|
|
20
|
+
"Operation",
|
|
21
|
+
"CommandGroup",
|
|
22
|
+
]
|