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/generator.py ADDED
@@ -0,0 +1,192 @@
1
+ #
2
+ # plating/generator.py
3
+ #
4
+ """Main documentation generator class and entry point."""
5
+
6
+ from pathlib import Path
7
+
8
+ from provide.foundation import pout
9
+
10
+ from plating.plating import PlatingDiscovery
11
+ from plating.models import FunctionInfo, ProviderInfo, ResourceInfo
12
+ from plating.schema import SchemaProcessor
13
+ from plating.templates import TemplateProcessor
14
+
15
+
16
+ class DocsGenerator:
17
+ """Main documentation generator class."""
18
+
19
+ def __init__(
20
+ self,
21
+ provider_dir: Path,
22
+ provider_name: str | None = None,
23
+ rendered_provider_name: str | None = None,
24
+ examples_dir: str = "examples",
25
+ templates_dir: str = "templates",
26
+ output_dir: str = "docs",
27
+ ignore_deprecated: bool = False,
28
+ ):
29
+ self.provider_dir = Path(provider_dir).resolve()
30
+ self.examples_dir = self.provider_dir / examples_dir
31
+ self.templates_dir = self.provider_dir / templates_dir
32
+ self.output_dir = self.provider_dir / output_dir
33
+ self.ignore_deprecated = ignore_deprecated
34
+
35
+ # Determine provider name
36
+ if provider_name:
37
+ self.provider_name = provider_name
38
+ else:
39
+ # Extract from directory name, removing terraform-provider- prefix
40
+ dir_name = self.provider_dir.name
41
+ if dir_name.startswith("terraform-provider-"):
42
+ self.provider_name = dir_name[19:] # Remove "terraform-provider-"
43
+ else:
44
+ self.provider_name = dir_name
45
+
46
+ self.rendered_provider_name = rendered_provider_name or self.provider_name
47
+
48
+ # Internal state
49
+ self.provider_schema = None
50
+ self.provider_info: ProviderInfo | None = None
51
+ self.resources: dict[str, ResourceInfo] = {}
52
+ self.data_sources: dict[str, ResourceInfo] = {}
53
+ self.functions: dict[str, FunctionInfo] = {}
54
+
55
+ # Initialize processors
56
+ self.schema_processor = SchemaProcessor(self)
57
+ self.template_processor = TemplateProcessor(self)
58
+ self.plating_discovery = PlatingDiscovery()
59
+
60
+ def process_examples(self):
61
+ """Process example files and associate them with resources/data sources."""
62
+ if not self.examples_dir.exists():
63
+ return
64
+
65
+ # Process provider examples
66
+ provider_example = None
67
+ if (self.examples_dir / "provider" / "provider.tf").exists():
68
+ provider_example = (
69
+ self.examples_dir / "provider" / "provider.tf"
70
+ ).read_text()
71
+ elif (self.examples_dir / "provider.tf").exists():
72
+ provider_example = (self.examples_dir / "provider.tf").read_text()
73
+
74
+ if provider_example and self.provider_info:
75
+ self.provider_info.has_example = True
76
+ self.provider_info.example_file = provider_example
77
+
78
+ # Process resource examples
79
+ for resource_name, resource_info in self.resources.items():
80
+ example_patterns = [
81
+ self.examples_dir / "resources" / f"{resource_name}" / "resource.tf",
82
+ self.examples_dir / "resources" / f"{resource_name}.tf",
83
+ self.examples_dir / f"{resource_name}.tf",
84
+ ]
85
+
86
+ for pattern in example_patterns:
87
+ if pattern.exists():
88
+ resource_info.has_example = True
89
+ resource_info.example_file = pattern.read_text()
90
+ break
91
+
92
+ # Process import examples
93
+ import_patterns = [
94
+ self.examples_dir / "resources" / f"{resource_name}" / "import.sh",
95
+ self.examples_dir / "resources" / f"{resource_name}_import.sh",
96
+ self.examples_dir / f"{resource_name}_import.sh",
97
+ ]
98
+
99
+ for pattern in import_patterns:
100
+ if pattern.exists():
101
+ resource_info.has_import = True
102
+ resource_info.import_file = pattern.read_text()
103
+ break
104
+
105
+ # Process data source examples
106
+ for ds_name, ds_info in self.data_sources.items():
107
+ example_patterns = [
108
+ self.examples_dir / "data-sources" / f"{ds_name}" / "data-source.tf",
109
+ self.examples_dir / "data-sources" / f"{ds_name}.tf",
110
+ self.examples_dir / f"{ds_name}.tf",
111
+ ]
112
+
113
+ for pattern in example_patterns:
114
+ if pattern.exists():
115
+ ds_info.has_example = True
116
+ ds_info.example_file = pattern.read_text()
117
+ break
118
+
119
+ # Process function examples
120
+ for func_name, func_info in self.functions.items():
121
+ example_patterns = [
122
+ self.examples_dir / "functions" / f"{func_name}" / "function.tf",
123
+ self.examples_dir / "functions" / f"{func_name}.tf",
124
+ self.examples_dir / f"{func_name}.tf",
125
+ ]
126
+
127
+ for pattern in example_patterns:
128
+ if pattern.exists():
129
+ func_info.has_example = True
130
+ func_info.example_file = pattern.read_text()
131
+ break
132
+
133
+ def generate(self):
134
+ """Generate documentation for the provider."""
135
+ pout(f"🔍 Generating documentation for {self.provider_name} provider...")
136
+
137
+ # Extract provider schema
138
+ pout("📋 Extracting provider schema...")
139
+ self.provider_schema = self.schema_processor.extract_provider_schema()
140
+
141
+ # Parse schema into our internal structures
142
+ self.schema_processor.parse_provider_schema()
143
+
144
+ # Process examples
145
+ pout("📁 Processing examples...")
146
+ self.process_examples()
147
+
148
+ # Generate missing templates
149
+ pout("📄 Generating missing templates...")
150
+ self.template_processor.generate_missing_templates()
151
+
152
+ # Render templates
153
+ pout("🎨 Rendering templates...")
154
+ self.template_processor.render_templates()
155
+
156
+ pout(f"✅ Documentation generated successfully in {self.output_dir}")
157
+
158
+
159
+ def generate_docs(
160
+ provider_dir: Path = Path(),
161
+ provider_name: str | None = None,
162
+ rendered_provider_name: str | None = None,
163
+ examples_dir: str = "examples",
164
+ templates_dir: str = "templates",
165
+ output_dir: str = "docs",
166
+ ignore_deprecated: bool = False,
167
+ ) -> None:
168
+ """Generate documentation for a Pyvider provider.
169
+
170
+ Args:
171
+ provider_dir: Path to the provider directory
172
+ provider_name: Name of the provider (auto-detected if None)
173
+ rendered_provider_name: Display name for the provider
174
+ examples_dir: Directory containing example files
175
+ templates_dir: Directory containing template files
176
+ output_dir: Directory to output generated documentation
177
+ ignore_deprecated: Whether to skip deprecated resources
178
+ """
179
+ generator = DocsGenerator(
180
+ provider_dir=provider_dir,
181
+ provider_name=provider_name,
182
+ rendered_provider_name=rendered_provider_name,
183
+ examples_dir=examples_dir,
184
+ templates_dir=templates_dir,
185
+ output_dir=output_dir,
186
+ ignore_deprecated=ignore_deprecated,
187
+ )
188
+
189
+ generator.generate()
190
+
191
+
192
+ # 🍲🥄📄🪄
plating/linting.py ADDED
@@ -0,0 +1,235 @@
1
+ #
2
+ # plating/linting.py
3
+ #
4
+ """Markdown linting integration for documentation generation."""
5
+
6
+ import json
7
+ from pathlib import Path
8
+ import subprocess
9
+
10
+
11
+ class MarkdownLinter:
12
+ """Handles markdown linting for generated documentation."""
13
+
14
+ def __init__(self, config_file: Path | None = None):
15
+ self.config_file = config_file
16
+
17
+ def lint_templates(self, template_dir: Path) -> tuple[bool, list[dict]]:
18
+ """Lint template files before generation.
19
+
20
+ Args:
21
+ template_dir: Directory containing template files
22
+
23
+ Returns:
24
+ Tuple of (success, errors) where errors is a list of error dictionaries
25
+ """
26
+ return self._run_markdownlint(f"{template_dir}/**/*.tmpl.md")
27
+
28
+ def lint_generated_docs(self, output_dir: Path) -> tuple[bool, list[dict]]:
29
+ """Lint generated documentation files.
30
+
31
+ Args:
32
+ output_dir: Directory containing generated markdown files
33
+
34
+ Returns:
35
+ Tuple of (success, errors) where errors is a list of error dictionaries
36
+ """
37
+ return self._run_markdownlint(f"{output_dir}/**/*.md")
38
+
39
+ def auto_fix_templates(self, template_dir: Path) -> bool:
40
+ """Attempt to auto-fix template linting issues.
41
+
42
+ Args:
43
+ template_dir: Directory containing template files
44
+
45
+ Returns:
46
+ True if fixes were applied successfully
47
+ """
48
+ return self._run_markdownlint_fix(f"{template_dir}/**/*.tmpl.md")
49
+
50
+ def auto_fix_generated_docs(self, output_dir: Path) -> bool:
51
+ """Attempt to auto-fix generated documentation linting issues.
52
+
53
+ Args:
54
+ output_dir: Directory containing generated markdown files
55
+
56
+ Returns:
57
+ True if fixes were applied successfully
58
+ """
59
+ return self._run_markdownlint_fix(f"{output_dir}/**/*.md")
60
+
61
+ def _run_markdownlint(self, pattern: str) -> tuple[bool, list[dict]]:
62
+ """Run markdownlint-cli2 on files matching pattern.
63
+
64
+ Args:
65
+ pattern: Glob pattern for files to lint
66
+
67
+ Returns:
68
+ Tuple of (success, errors)
69
+ """
70
+ cmd = ["markdownlint-cli2", pattern]
71
+ if self.config_file:
72
+ cmd.extend(["--config", str(self.config_file)])
73
+
74
+ try:
75
+ result = subprocess.run(cmd, capture_output=True, text=True, check=False)
76
+
77
+ errors = []
78
+ if result.returncode != 0:
79
+ # Parse error output
80
+ for line in result.stderr.split("\n"):
81
+ if line.strip() and ":" in line:
82
+ parts = line.split(":", 3)
83
+ if len(parts) >= 4:
84
+ errors.append(
85
+ {
86
+ "file": parts[0],
87
+ "line": parts[1] if parts[1].isdigit() else None,
88
+ "column": parts[2] if parts[2].isdigit() else None,
89
+ "rule": parts[3].split()[0]
90
+ if parts[3]
91
+ else "unknown",
92
+ "message": parts[3]
93
+ if parts[3]
94
+ else "Unknown error",
95
+ }
96
+ )
97
+
98
+ return result.returncode == 0, errors
99
+
100
+ except FileNotFoundError:
101
+ raise RuntimeError(
102
+ "markdownlint-cli2 not found. Install with: npm install -g markdownlint-cli2"
103
+ )
104
+
105
+ def _run_markdownlint_fix(self, pattern: str) -> bool:
106
+ """Run markdownlint-cli2 with --fix flag.
107
+
108
+ Args:
109
+ pattern: Glob pattern for files to fix
110
+
111
+ Returns:
112
+ True if fixes were applied successfully
113
+ """
114
+ cmd = ["markdownlint-cli2", "--fix", pattern]
115
+ if self.config_file:
116
+ cmd.extend(["--config", str(self.config_file)])
117
+
118
+ try:
119
+ result = subprocess.run(cmd, capture_output=True, text=True, check=False)
120
+
121
+ return result.returncode == 0
122
+
123
+ except FileNotFoundError:
124
+ raise RuntimeError(
125
+ "markdownlint-cli2 not found. Install with: npm install -g markdownlint-cli2"
126
+ )
127
+
128
+ def generate_lint_report(self, errors: list[dict], output_file: Path) -> None:
129
+ """Generate a JSON report of linting errors for CI/CD integration.
130
+
131
+ Args:
132
+ errors: List of error dictionaries from linting
133
+ output_file: Path to write JSON report
134
+ """
135
+ report = {
136
+ "total_errors": len(errors),
137
+ "errors_by_rule": {},
138
+ "errors_by_file": {},
139
+ "errors": errors,
140
+ }
141
+
142
+ # Group errors by rule
143
+ for error in errors:
144
+ rule = error.get("rule", "unknown")
145
+ if rule not in report["errors_by_rule"]:
146
+ report["errors_by_rule"][rule] = 0
147
+ report["errors_by_rule"][rule] += 1
148
+
149
+ # Group errors by file
150
+ for error in errors:
151
+ file_path = error.get("file", "unknown")
152
+ if file_path not in report["errors_by_file"]:
153
+ report["errors_by_file"][file_path] = 0
154
+ report["errors_by_file"][file_path] += 1
155
+
156
+ with open(output_file, "w") as f:
157
+ json.dump(report, f, indent=2)
158
+
159
+
160
+ def apply_markdown_fixes(content: str) -> str:
161
+ """Apply common markdown fixes to content.
162
+
163
+ Args:
164
+ content: Markdown content to fix
165
+
166
+ Returns:
167
+ Fixed markdown content
168
+ """
169
+ # Ensure trailing newline
170
+ content = content.rstrip() + "\n"
171
+
172
+ # Fix list marker spacing (convert double spaces to single)
173
+ import re
174
+
175
+ content = re.sub(r"^(\s*)- ", r"\1- ", content, flags=re.MULTILINE)
176
+ content = re.sub(r"^(\s*)\d+\. ", r"\1\d+. ", content, flags=re.MULTILINE)
177
+
178
+ # Add blank lines around headings
179
+ content = re.sub(r"\n(#{1,6}\s+.*)\n(?!\n)", r"\n\1\n\n", content)
180
+ content = re.sub(r"(?<!\n)\n(#{1,6}\s+.*)\n", r"\n\n\1\n", content)
181
+
182
+ # Add blank lines around fenced code blocks
183
+ content = re.sub(r"\n```(\w*)\n(?!\n)", r"\n\n```\1\n", content)
184
+ content = re.sub(r"(?<!\n)\n```(\w*)\n", r"\n\n```\1\n", content)
185
+ content = re.sub(r"\n```\n(?!\n)", r"\n```\n\n", content)
186
+ content = re.sub(r"(?<!\n)\n```\n", r"\n\n```\n", content)
187
+
188
+ return content
189
+
190
+
191
+ def break_long_lines(content: str, max_length: int = 100) -> str:
192
+ """Break long lines at word boundaries.
193
+
194
+ Args:
195
+ content: Markdown content
196
+ max_length: Maximum line length
197
+
198
+ Returns:
199
+ Content with lines broken at word boundaries
200
+ """
201
+ lines = content.split("\n")
202
+ fixed_lines = []
203
+
204
+ for line in lines:
205
+ if len(line) <= max_length:
206
+ fixed_lines.append(line)
207
+ continue
208
+
209
+ # Skip code blocks and headings
210
+ if line.startswith("```") or line.startswith("#"):
211
+ fixed_lines.append(line)
212
+ continue
213
+
214
+ # Break at word boundaries
215
+ words = line.split(" ")
216
+ current_line = ""
217
+
218
+ for word in words:
219
+ if len(current_line + " " + word) <= max_length:
220
+ if current_line:
221
+ current_line += " " + word
222
+ else:
223
+ current_line = word
224
+ else:
225
+ if current_line:
226
+ fixed_lines.append(current_line)
227
+ current_line = word
228
+
229
+ if current_line:
230
+ fixed_lines.append(current_line)
231
+
232
+ return "\n".join(fixed_lines)
233
+
234
+
235
+ # 🍲🥄📄🪄
plating/models.py ADDED
@@ -0,0 +1,75 @@
1
+ #
2
+ # plating/models.py
3
+ #
4
+ """Data models for documentation generation."""
5
+
6
+ from typing import Any
7
+
8
+ from attrs import define, field
9
+
10
+
11
+ @define
12
+ class ProviderInfo:
13
+ """Information about a Pyvider provider."""
14
+
15
+ name: str
16
+ description: str
17
+ short_name: str = ""
18
+ rendered_name: str = ""
19
+ schema_markdown: str = ""
20
+ has_example: bool = False
21
+ example_file: str = ""
22
+
23
+ def __attrs_post_init__(self):
24
+ if not self.short_name:
25
+ self.short_name = self.name
26
+ if not self.rendered_name:
27
+ self.rendered_name = self.name
28
+
29
+
30
+ @define
31
+ class ResourceInfo:
32
+ """Information about a resource or data source."""
33
+
34
+ name: str
35
+ type: str # "Resource", "Data Source", or "Function"
36
+ description: str
37
+ schema_markdown: str = ""
38
+ schema: dict[str, Any] | None = None
39
+ has_example: bool = False
40
+ example_file: str = ""
41
+ has_import: bool = False
42
+ import_file: str = ""
43
+ has_import_id_config: bool = False
44
+ import_id_config_file: str = ""
45
+ has_import_identity_config: bool = False
46
+ import_identity_config_file: str = ""
47
+ # Co-located documentation fields
48
+ examples: dict[str, str] = field(factory=dict)
49
+ import_docs: str = ""
50
+ colocated_notes: str = ""
51
+ migration: str = ""
52
+
53
+
54
+ @define
55
+ class FunctionInfo:
56
+ """Information about a provider-defined function."""
57
+
58
+ name: str
59
+ type: str = "Function"
60
+ description: str = ""
61
+ summary: str = ""
62
+ has_example: bool = False
63
+ example_file: str = ""
64
+ signature_markdown: str = ""
65
+ arguments_markdown: str = ""
66
+ has_variadic: bool = False
67
+ variadic_argument_markdown: str = ""
68
+ # Co-located documentation fields
69
+ examples: dict[str, str] = field(factory=dict)
70
+ import_docs: str = ""
71
+ colocated_notes: str = ""
72
+ migration: str = ""
73
+
74
+
75
+ # 🍲🥄📊🪄