plating 0.0.0.dev0__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.
- plating/__init__.py +27 -0
- plating/_version.py +53 -0
- plating/adorner/__init__.py +16 -0
- plating/adorner/adorner.py +115 -0
- plating/adorner/api.py +24 -0
- plating/adorner/finder.py +22 -0
- plating/adorner/templates.py +199 -0
- plating/cli.py +317 -0
- plating/config.py +106 -0
- plating/error_handling.py +125 -0
- plating/errors.py +142 -0
- plating/generator.py +192 -0
- plating/linting.py +235 -0
- plating/models.py +75 -0
- plating/plater.py +487 -0
- plating/plating.py +207 -0
- plating/schema.py +502 -0
- plating/template_filters.py +117 -0
- plating/template_functions.py +216 -0
- plating/templates.py +232 -0
- plating/test_runner.py +1005 -0
- plating/types.py +71 -0
- plating-0.0.0.dev0.dist-info/METADATA +223 -0
- plating-0.0.0.dev0.dist-info/RECORD +28 -0
- plating-0.0.0.dev0.dist-info/WHEEL +5 -0
- plating-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- plating-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- plating-0.0.0.dev0.dist-info/top_level.txt +1 -0
plating/cli.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# plating/cli.py
|
|
4
|
+
#
|
|
5
|
+
"""CLI interface for documentation generation."""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from provide.foundation import logger, pout, perr
|
|
11
|
+
|
|
12
|
+
from plating.adorner import adorn_components
|
|
13
|
+
from plating.errors import PlatingError, handle_error
|
|
14
|
+
from plating.plater import generate_docs
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def main() -> None:
|
|
19
|
+
"""Plating - Documentation generator for Terraform/OpenTofu providers."""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@main.command("adorn")
|
|
24
|
+
@click.option(
|
|
25
|
+
"--component-type",
|
|
26
|
+
type=click.Choice(["resource", "data_source", "function"]),
|
|
27
|
+
multiple=True,
|
|
28
|
+
help="Filter by component type (can be used multiple times).",
|
|
29
|
+
)
|
|
30
|
+
def adorn_command(component_type: tuple[str, ...]) -> None:
|
|
31
|
+
"""Adorn components with missing .plating directories."""
|
|
32
|
+
try:
|
|
33
|
+
pout("đ Adorning components with .plating directories...")
|
|
34
|
+
|
|
35
|
+
component_types = list(component_type) if component_type else None
|
|
36
|
+
results = adorn_components(component_types)
|
|
37
|
+
|
|
38
|
+
total = sum(results.values())
|
|
39
|
+
if total > 0:
|
|
40
|
+
pout(f"â
Adorned {total} components:")
|
|
41
|
+
for comp_type, count in results.items():
|
|
42
|
+
if count > 0:
|
|
43
|
+
pout(f" - {count} {comp_type}{'s' if count != 1 else ''}")
|
|
44
|
+
else:
|
|
45
|
+
pout("âšī¸ No missing .plating directories found")
|
|
46
|
+
|
|
47
|
+
click.secho("â
Adorning completed successfully!", fg="green")
|
|
48
|
+
|
|
49
|
+
except PlatingError as e:
|
|
50
|
+
# Our custom errors have good messages
|
|
51
|
+
click.secho(f"â {e}", fg="red", err=True)
|
|
52
|
+
logger.error(f"Adorning failed: {e}")
|
|
53
|
+
raise click.Abort() from e
|
|
54
|
+
except Exception as e:
|
|
55
|
+
import traceback
|
|
56
|
+
|
|
57
|
+
error_msg = handle_error(e, logger)
|
|
58
|
+
click.secho(f"â Adorning failed: {error_msg}", fg="red", err=True)
|
|
59
|
+
click.secho(f"Stack trace:\n{traceback.format_exc()}", fg="red", err=True)
|
|
60
|
+
raise click.Abort() from e
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@main.command("plate")
|
|
64
|
+
@click.option(
|
|
65
|
+
"--output-dir",
|
|
66
|
+
type=click.Path(file_okay=False, resolve_path=True),
|
|
67
|
+
default="docs",
|
|
68
|
+
help="Output directory for plated documentation.",
|
|
69
|
+
)
|
|
70
|
+
@click.option(
|
|
71
|
+
"--provider-dir",
|
|
72
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
73
|
+
default=".",
|
|
74
|
+
help="Path to the provider directory.",
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--component-type",
|
|
78
|
+
type=click.Choice(["resource", "data_source", "function"]),
|
|
79
|
+
multiple=True,
|
|
80
|
+
help="Filter by component type (can be used multiple times).",
|
|
81
|
+
)
|
|
82
|
+
@click.option(
|
|
83
|
+
"--force",
|
|
84
|
+
is_flag=True,
|
|
85
|
+
default=False,
|
|
86
|
+
help="Force documentation generation even if not in a provider directory.",
|
|
87
|
+
)
|
|
88
|
+
def plate_command(
|
|
89
|
+
output_dir: str, provider_dir: str, component_type: tuple[str, ...], force: bool
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Plate all plating bundles into final documentation."""
|
|
92
|
+
try:
|
|
93
|
+
provider_path = Path(provider_dir)
|
|
94
|
+
|
|
95
|
+
# Validate that we're in a provider directory unless --force is used
|
|
96
|
+
if not force and not _is_provider_directory(provider_path):
|
|
97
|
+
click.secho(
|
|
98
|
+
"â This does not appear to be a provider directory. "
|
|
99
|
+
"Expected to find a pyproject.toml with provider configuration "
|
|
100
|
+
"or templates directory. Use --force to override.",
|
|
101
|
+
fg="red",
|
|
102
|
+
err=True,
|
|
103
|
+
)
|
|
104
|
+
raise click.Abort()
|
|
105
|
+
|
|
106
|
+
pout("đŊī¸ Plating documentation...")
|
|
107
|
+
generate_docs(output_dir=output_dir)
|
|
108
|
+
click.secho("â
Documentation plated successfully!", fg="green")
|
|
109
|
+
|
|
110
|
+
except PlatingError as e:
|
|
111
|
+
# Our custom errors have good messages
|
|
112
|
+
click.secho(f"â {e}", fg="red", err=True)
|
|
113
|
+
logger.error(f"Documentation plating failed: {e}")
|
|
114
|
+
raise click.Abort() from e
|
|
115
|
+
except Exception as e:
|
|
116
|
+
import traceback
|
|
117
|
+
|
|
118
|
+
error_msg = handle_error(e, logger)
|
|
119
|
+
click.secho(f"â Documentation plating failed: {error_msg}", fg="red", err=True)
|
|
120
|
+
click.secho(f"Stack trace:\n{traceback.format_exc()}", fg="red", err=True)
|
|
121
|
+
raise click.Abort() from e
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _is_provider_directory(path: Path) -> bool:
|
|
125
|
+
"""Check if the given path appears to be a provider directory."""
|
|
126
|
+
# Check for pyproject.toml with terraform-provider or pyvider in name
|
|
127
|
+
pyproject_toml = path / "pyproject.toml"
|
|
128
|
+
if pyproject_toml.exists():
|
|
129
|
+
try:
|
|
130
|
+
import tomllib
|
|
131
|
+
|
|
132
|
+
with open(pyproject_toml, "rb") as f:
|
|
133
|
+
data = tomllib.load(f)
|
|
134
|
+
|
|
135
|
+
# Check project name for provider indicators
|
|
136
|
+
project_name = data.get("project", {}).get("name", "")
|
|
137
|
+
if "terraform-provider" in project_name or "pyvider" in project_name:
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
# Check for pyvider configuration
|
|
141
|
+
if "tool" in data and "pyvider" in data["tool"]:
|
|
142
|
+
return True
|
|
143
|
+
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
# Check for templates directory (common in provider repos)
|
|
148
|
+
if (path / "templates").exists():
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
# Check for provider-specific files
|
|
152
|
+
provider_indicators = [
|
|
153
|
+
"terraform-registry-manifest.json",
|
|
154
|
+
"pyvider.toml",
|
|
155
|
+
".plating",
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
for indicator in provider_indicators:
|
|
159
|
+
if (path / indicator).exists():
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Add 'render' as an alias for 'plate' for backward compatibility
|
|
166
|
+
@main.command("render", hidden=True) # Hidden from help but still works
|
|
167
|
+
@click.option(
|
|
168
|
+
"--output-dir",
|
|
169
|
+
type=click.Path(file_okay=False),
|
|
170
|
+
default="docs",
|
|
171
|
+
help="Output directory for plated documentation.",
|
|
172
|
+
)
|
|
173
|
+
@click.option(
|
|
174
|
+
"--provider-dir",
|
|
175
|
+
type=click.Path(exists=True, file_okay=False, resolve_path=True),
|
|
176
|
+
default=".",
|
|
177
|
+
help="Path to the provider directory.",
|
|
178
|
+
)
|
|
179
|
+
@click.option(
|
|
180
|
+
"--component-type",
|
|
181
|
+
type=click.Choice(["resource", "data_source", "function"]),
|
|
182
|
+
multiple=True,
|
|
183
|
+
help="Filter by component type (can be used multiple times).",
|
|
184
|
+
)
|
|
185
|
+
@click.option(
|
|
186
|
+
"--force",
|
|
187
|
+
is_flag=True,
|
|
188
|
+
default=False,
|
|
189
|
+
help="Force documentation generation even if not in a provider directory.",
|
|
190
|
+
)
|
|
191
|
+
def render_command(
|
|
192
|
+
output_dir: str, provider_dir: str, component_type: tuple[str, ...], force: bool
|
|
193
|
+
) -> None:
|
|
194
|
+
"""(Deprecated) Alias for 'plate' command. Use 'garnish plate' instead."""
|
|
195
|
+
click.echo("Note: 'render' is deprecated. Please use 'plate' instead.")
|
|
196
|
+
# Call the plate command directly
|
|
197
|
+
ctx = click.get_current_context()
|
|
198
|
+
ctx.invoke(
|
|
199
|
+
plate_command,
|
|
200
|
+
output_dir=output_dir,
|
|
201
|
+
provider_dir=provider_dir,
|
|
202
|
+
component_type=component_type,
|
|
203
|
+
force=force,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@main.command("test")
|
|
208
|
+
@click.option(
|
|
209
|
+
"--component-type",
|
|
210
|
+
type=click.Choice(["resource", "data_source", "function"]),
|
|
211
|
+
multiple=True,
|
|
212
|
+
help="Filter by component type (can be used multiple times).",
|
|
213
|
+
)
|
|
214
|
+
@click.option(
|
|
215
|
+
"--parallel",
|
|
216
|
+
type=int,
|
|
217
|
+
default=4,
|
|
218
|
+
help="Number of tests to run in parallel.",
|
|
219
|
+
)
|
|
220
|
+
@click.option(
|
|
221
|
+
"--output-dir",
|
|
222
|
+
type=click.Path(file_okay=False),
|
|
223
|
+
default=".plating-tests",
|
|
224
|
+
help="Temporary directory for test execution.",
|
|
225
|
+
)
|
|
226
|
+
@click.option(
|
|
227
|
+
"--output-file",
|
|
228
|
+
type=click.Path(dir_okay=False),
|
|
229
|
+
help="File to write test results to.",
|
|
230
|
+
)
|
|
231
|
+
@click.option(
|
|
232
|
+
"--output-format",
|
|
233
|
+
type=click.Choice(["json", "markdown", "html"]),
|
|
234
|
+
default="json",
|
|
235
|
+
help="Format for test results output.",
|
|
236
|
+
)
|
|
237
|
+
def test_command(
|
|
238
|
+
component_type: tuple[str, ...],
|
|
239
|
+
parallel: int,
|
|
240
|
+
output_dir: str,
|
|
241
|
+
output_file: str | None,
|
|
242
|
+
output_format: str,
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Run all plating example files as Terraform tests."""
|
|
245
|
+
try:
|
|
246
|
+
# Import here to avoid circular imports
|
|
247
|
+
from .test_runner import run_plating_tests
|
|
248
|
+
|
|
249
|
+
component_types = list(component_type) if component_type else None
|
|
250
|
+
results = run_plating_tests(
|
|
251
|
+
component_types=component_types,
|
|
252
|
+
parallel=parallel,
|
|
253
|
+
output_dir=Path(output_dir),
|
|
254
|
+
output_file=Path(output_file) if output_file else None,
|
|
255
|
+
output_format=output_format,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Display results
|
|
259
|
+
total_tests = results["total"]
|
|
260
|
+
passed = results["passed"]
|
|
261
|
+
failed = results["failed"]
|
|
262
|
+
warnings = results.get("warnings", 0)
|
|
263
|
+
skipped = results.get("skipped", 0)
|
|
264
|
+
|
|
265
|
+
if total_tests == 0:
|
|
266
|
+
pout("âšī¸ No plating examples found to test")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
pout("\nđ Test Results:")
|
|
270
|
+
pout(f" Total: {total_tests}")
|
|
271
|
+
pout(f" â
Passed: {passed}")
|
|
272
|
+
if failed > 0:
|
|
273
|
+
pout(f" â Failed: {failed}")
|
|
274
|
+
if warnings > 0:
|
|
275
|
+
pout(f" â ī¸ Warnings: {warnings}")
|
|
276
|
+
if skipped > 0:
|
|
277
|
+
pout(f" âī¸ Skipped: {skipped}")
|
|
278
|
+
|
|
279
|
+
# Show warnings if any
|
|
280
|
+
if warnings > 0:
|
|
281
|
+
pout("\nâ ī¸ Tests with warnings:")
|
|
282
|
+
for test_name, details in results.get("test_details", {}).items():
|
|
283
|
+
if details.get("warnings"):
|
|
284
|
+
pout(f" - {test_name} ({len(details['warnings'])} warnings)")
|
|
285
|
+
for warning in details["warnings"][:2]: # Show first 2 warnings
|
|
286
|
+
pout(f" âĸ {warning['message']}")
|
|
287
|
+
if len(details["warnings"]) > 2:
|
|
288
|
+
pout(f" âĸ ... and {len(details['warnings']) - 2} more")
|
|
289
|
+
|
|
290
|
+
if failed > 0:
|
|
291
|
+
pout("\nâ Failed tests:")
|
|
292
|
+
for test_name, error in results["failures"].items():
|
|
293
|
+
pout(f" - {test_name}: {error}")
|
|
294
|
+
click.secho("\nâ Some tests failed!", fg="red", err=True)
|
|
295
|
+
raise click.Abort()
|
|
296
|
+
else:
|
|
297
|
+
click.secho("\nâ
All tests passed!", fg="green")
|
|
298
|
+
|
|
299
|
+
except PlatingError as e:
|
|
300
|
+
# Our custom errors have good messages
|
|
301
|
+
click.secho(f"â {e}", fg="red", err=True)
|
|
302
|
+
logger.error(f"Test execution failed: {e}")
|
|
303
|
+
raise click.Abort() from e
|
|
304
|
+
except Exception as e:
|
|
305
|
+
import traceback
|
|
306
|
+
|
|
307
|
+
error_msg = handle_error(e, logger)
|
|
308
|
+
click.secho(f"â Test execution failed: {error_msg}", fg="red", err=True)
|
|
309
|
+
click.secho(f"Stack trace:\n{traceback.format_exc()}", fg="red", err=True)
|
|
310
|
+
raise click.Abort() from e
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
main()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# đĨđđĒ
|
plating/config.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/config.py
|
|
3
|
+
#
|
|
4
|
+
"""Configuration management for plating."""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from attrs import define
|
|
11
|
+
from provide.foundation.config import RuntimeConfig, field
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@define
|
|
15
|
+
class PlatingConfig(RuntimeConfig):
|
|
16
|
+
"""Configuration for plating operations."""
|
|
17
|
+
|
|
18
|
+
# Terraform/OpenTofu configuration
|
|
19
|
+
terraform_binary: str | None = field(
|
|
20
|
+
default=None,
|
|
21
|
+
description="Path to terraform/tofu binary",
|
|
22
|
+
env_var="GARNISH_TF_BINARY"
|
|
23
|
+
)
|
|
24
|
+
plugin_cache_dir: Path | None = field(
|
|
25
|
+
default=None,
|
|
26
|
+
description="Terraform plugin cache directory",
|
|
27
|
+
env_var="TF_PLUGIN_CACHE_DIR"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Test execution configuration
|
|
31
|
+
test_timeout: int = field(
|
|
32
|
+
default=120,
|
|
33
|
+
description="Timeout for test execution in seconds",
|
|
34
|
+
env_var="GARNISH_TEST_TIMEOUT"
|
|
35
|
+
)
|
|
36
|
+
test_parallel: int = field(
|
|
37
|
+
default=4,
|
|
38
|
+
description="Number of parallel test executions",
|
|
39
|
+
env_var="GARNISH_TEST_PARALLEL"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Output configuration
|
|
43
|
+
output_dir: Path = field(
|
|
44
|
+
default=Path("./docs"),
|
|
45
|
+
description="Default output directory for documentation",
|
|
46
|
+
env_var="GARNISH_OUTPUT_DIR"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Component directories
|
|
50
|
+
resources_dir: Path = field(
|
|
51
|
+
default=Path("./resources"),
|
|
52
|
+
description="Directory containing resource definitions"
|
|
53
|
+
)
|
|
54
|
+
data_sources_dir: Path = field(
|
|
55
|
+
default=Path("./data_sources"),
|
|
56
|
+
description="Directory containing data source definitions"
|
|
57
|
+
)
|
|
58
|
+
functions_dir: Path = field(
|
|
59
|
+
default=Path("./functions"),
|
|
60
|
+
description="Directory containing function definitions"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def __attrs_post_init__(self) -> None:
|
|
64
|
+
"""Initialize derived configuration values."""
|
|
65
|
+
super().__attrs_post_init__()
|
|
66
|
+
|
|
67
|
+
# Auto-detect terraform binary if not specified
|
|
68
|
+
if self.terraform_binary is None:
|
|
69
|
+
import shutil
|
|
70
|
+
self.terraform_binary = (
|
|
71
|
+
shutil.which("tofu") or
|
|
72
|
+
shutil.which("terraform") or
|
|
73
|
+
"terraform"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Set default plugin cache directory
|
|
77
|
+
if self.plugin_cache_dir is None:
|
|
78
|
+
self.plugin_cache_dir = Path.home() / ".terraform.d" / "plugin-cache"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_terraform_env(self) -> dict[str, str]:
|
|
82
|
+
"""Get environment variables for terraform execution."""
|
|
83
|
+
env = os.environ.copy()
|
|
84
|
+
|
|
85
|
+
if self.plugin_cache_dir and self.plugin_cache_dir.exists():
|
|
86
|
+
env["TF_PLUGIN_CACHE_DIR"] = str(self.plugin_cache_dir)
|
|
87
|
+
|
|
88
|
+
return env
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Global configuration instance
|
|
92
|
+
_config: PlatingConfig | None = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_config() -> PlatingConfig:
|
|
96
|
+
"""Get the global configuration instance."""
|
|
97
|
+
global _config
|
|
98
|
+
if _config is None:
|
|
99
|
+
_config = PlatingConfig.from_env()
|
|
100
|
+
return _config
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def set_config(config: PlatingConfig) -> None:
|
|
104
|
+
"""Set the global configuration instance."""
|
|
105
|
+
global _config
|
|
106
|
+
_config = config
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/error_handling.py
|
|
3
|
+
#
|
|
4
|
+
"""Centralized error handling and reporting for plating."""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ErrorReporter:
|
|
15
|
+
"""Centralized error reporting for plating operations."""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def report_subprocess_error(
|
|
19
|
+
cmd: list[str],
|
|
20
|
+
error: subprocess.CalledProcessError,
|
|
21
|
+
context: str = ""
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Report subprocess execution errors consistently."""
|
|
24
|
+
console.print(f"[red]Error executing command: {' '.join(cmd)}[/red]")
|
|
25
|
+
if context:
|
|
26
|
+
console.print(f"[yellow]Context: {context}[/yellow]")
|
|
27
|
+
if error.stderr:
|
|
28
|
+
console.print(f"[red]Error output:[/red]\n{error.stderr}")
|
|
29
|
+
if error.returncode:
|
|
30
|
+
console.print(f"[red]Exit code: {error.returncode}[/red]")
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def report_file_error(
|
|
34
|
+
path: Path,
|
|
35
|
+
operation: str,
|
|
36
|
+
error: Exception
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Report file operation errors consistently."""
|
|
39
|
+
console.print(f"[red]File operation failed: {operation}[/red]")
|
|
40
|
+
console.print(f"[yellow]Path: {path}[/yellow]")
|
|
41
|
+
console.print(f"[red]Error: {error}[/red]")
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def report_validation_error(
|
|
45
|
+
component: str,
|
|
46
|
+
errors: list[str],
|
|
47
|
+
warnings: list[str] | None = None
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Report validation errors and warnings consistently."""
|
|
50
|
+
console.print(f"[red]Validation failed for {component}[/red]")
|
|
51
|
+
for error in errors:
|
|
52
|
+
console.print(f" [red]â[/red] {error}")
|
|
53
|
+
if warnings:
|
|
54
|
+
for warning in warnings:
|
|
55
|
+
console.print(f" [yellow]â [/yellow] {warning}")
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def report_warning(message: str, details: str | None = None) -> None:
|
|
59
|
+
"""Report warnings consistently."""
|
|
60
|
+
console.print(f"[yellow]Warning: {message}[/yellow]")
|
|
61
|
+
if details:
|
|
62
|
+
console.print(f"[dim]{details}[/dim]")
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def report_success(message: str, details: str | None = None) -> None:
|
|
66
|
+
"""Report success messages consistently."""
|
|
67
|
+
console.print(f"[green]â {message}[/green]")
|
|
68
|
+
if details:
|
|
69
|
+
console.print(f"[dim]{details}[/dim]")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def handle_subprocess_execution(
|
|
73
|
+
cmd: list[str],
|
|
74
|
+
cwd: Path | None = None,
|
|
75
|
+
timeout: int = 120,
|
|
76
|
+
context: str = "",
|
|
77
|
+
capture_output: bool = True
|
|
78
|
+
) -> subprocess.CompletedProcess[str]:
|
|
79
|
+
"""Execute subprocess with consistent error handling.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
cmd: Command to execute
|
|
83
|
+
cwd: Working directory for command
|
|
84
|
+
timeout: Command timeout in seconds
|
|
85
|
+
context: Context description for error reporting
|
|
86
|
+
capture_output: Whether to capture stdout/stderr
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
CompletedProcess result
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
subprocess.CalledProcessError: If command fails
|
|
93
|
+
subprocess.TimeoutExpired: If command times out
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
result = subprocess.run(
|
|
97
|
+
cmd,
|
|
98
|
+
cwd=cwd,
|
|
99
|
+
capture_output=capture_output,
|
|
100
|
+
text=True,
|
|
101
|
+
timeout=timeout,
|
|
102
|
+
check=False
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if result.returncode != 0:
|
|
106
|
+
error = subprocess.CalledProcessError(
|
|
107
|
+
result.returncode, cmd, result.stdout, result.stderr
|
|
108
|
+
)
|
|
109
|
+
ErrorReporter.report_subprocess_error(cmd, error, context)
|
|
110
|
+
raise error
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
except subprocess.TimeoutExpired:
|
|
115
|
+
ErrorReporter.report_warning(
|
|
116
|
+
f"Command timed out after {timeout} seconds",
|
|
117
|
+
f"Command: {' '.join(cmd)}"
|
|
118
|
+
)
|
|
119
|
+
raise
|
|
120
|
+
except FileNotFoundError:
|
|
121
|
+
ErrorReporter.report_warning(
|
|
122
|
+
f"Command not found: {cmd[0]}",
|
|
123
|
+
"Please ensure the required tool is installed"
|
|
124
|
+
)
|
|
125
|
+
raise
|
plating/errors.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom error types for plating.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from provide.foundation.errors import FoundationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PlatingError(FoundationError):
|
|
11
|
+
"""Base error for all plating-related errors."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BundleError(PlatingError):
|
|
17
|
+
"""Error related to plating bundles."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, bundle_name: str, message: str):
|
|
20
|
+
self.bundle_name = bundle_name
|
|
21
|
+
super().__init__(f"Bundle '{bundle_name}': {message}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlatingRenderError(PlatingError):
|
|
25
|
+
"""Error during documentation plating."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, bundle_name: str, reason: str):
|
|
28
|
+
self.bundle_name = bundle_name
|
|
29
|
+
self.reason = reason
|
|
30
|
+
super().__init__(f"Failed to plate '{bundle_name}': {reason}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AdorningError(PlatingError):
|
|
34
|
+
"""Error during component adorning."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, component_name: str, component_type: str, reason: str):
|
|
37
|
+
self.component_name = component_name
|
|
38
|
+
self.component_type = component_type
|
|
39
|
+
self.reason = reason
|
|
40
|
+
super().__init__(
|
|
41
|
+
f"Failed to adorn {component_type} '{component_name}': {reason}"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class SchemaError(PlatingError):
|
|
46
|
+
"""Error related to schema extraction or processing."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, provider_name: str, reason: str):
|
|
49
|
+
self.provider_name = provider_name
|
|
50
|
+
self.reason = reason
|
|
51
|
+
super().__init__(f"Schema error for provider '{provider_name}': {reason}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TemplateError(PlatingError):
|
|
55
|
+
"""Error during template rendering."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, template_path: Path | str, reason: str):
|
|
58
|
+
self.template_path = template_path
|
|
59
|
+
self.reason = reason
|
|
60
|
+
super().__init__(f"Template error in '{template_path}': {reason}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DiscoveryError(PlatingError):
|
|
64
|
+
"""Error during bundle discovery."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, package_name: str, reason: str):
|
|
67
|
+
self.package_name = package_name
|
|
68
|
+
self.reason = reason
|
|
69
|
+
super().__init__(f"Discovery error for package '{package_name}': {reason}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ConfigurationError(PlatingError):
|
|
73
|
+
"""Error in plating configuration."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, config_key: str, reason: str):
|
|
76
|
+
self.config_key = config_key
|
|
77
|
+
self.reason = reason
|
|
78
|
+
super().__init__(f"Configuration error for '{config_key}': {reason}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestRunnerError(PlatingError):
|
|
82
|
+
"""Error during test execution."""
|
|
83
|
+
|
|
84
|
+
def __init__(self, test_name: str, reason: str):
|
|
85
|
+
self.test_name = test_name
|
|
86
|
+
self.reason = reason
|
|
87
|
+
super().__init__(f"Test '{test_name}' failed: {reason}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class FileSystemError(PlatingError):
|
|
91
|
+
"""Error related to file system operations."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, path: Path | str, operation: str, reason: str):
|
|
94
|
+
self.path = path
|
|
95
|
+
self.operation = operation
|
|
96
|
+
self.reason = reason
|
|
97
|
+
super().__init__(f"File system error during {operation} on '{path}': {reason}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def handle_error(error: Exception, logger: Any = None, reraise: bool = False) -> str:
|
|
101
|
+
"""
|
|
102
|
+
Handle an error with proper logging and optional re-raising.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
error: The exception to handle
|
|
106
|
+
logger: Optional logger instance to use
|
|
107
|
+
reraise: Whether to re-raise the error after handling
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
A formatted error message
|
|
111
|
+
"""
|
|
112
|
+
error_msg = str(error)
|
|
113
|
+
|
|
114
|
+
if isinstance(error, PlatingError):
|
|
115
|
+
# It's one of our custom errors, we have more context
|
|
116
|
+
if logger:
|
|
117
|
+
logger.error(error_msg)
|
|
118
|
+
elif isinstance(error, FileNotFoundError):
|
|
119
|
+
error_msg = f"File not found: {error}"
|
|
120
|
+
if logger:
|
|
121
|
+
logger.error(error_msg)
|
|
122
|
+
elif isinstance(error, PermissionError):
|
|
123
|
+
error_msg = f"Permission denied: {error}"
|
|
124
|
+
if logger:
|
|
125
|
+
logger.error(error_msg)
|
|
126
|
+
elif isinstance(error, (OSError, IOError)):
|
|
127
|
+
error_msg = f"I/O error: {error}"
|
|
128
|
+
if logger:
|
|
129
|
+
logger.error(error_msg)
|
|
130
|
+
else:
|
|
131
|
+
# Generic error
|
|
132
|
+
error_msg = f"Unexpected error: {error}"
|
|
133
|
+
if logger:
|
|
134
|
+
logger.exception("Unexpected error occurred")
|
|
135
|
+
|
|
136
|
+
if reraise:
|
|
137
|
+
raise
|
|
138
|
+
|
|
139
|
+
return error_msg
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# đ˛âđĒ
|