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/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
+ # 🍲❌đŸĒ„