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/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/__init__.py
|
|
3
|
+
#
|
|
4
|
+
"""Garnish - Documentation generation for Terraform/OpenTofu providers.
|
|
5
|
+
|
|
6
|
+
This package implements a comprehensive documentation generation system modeled after
|
|
7
|
+
HashiCorp's tfplugindocs tool. It extracts provider schemas, processes templates and
|
|
8
|
+
examples, and generates Terraform Registry-compliant documentation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from plating._version import __version__
|
|
12
|
+
|
|
13
|
+
from plating.cli import main
|
|
14
|
+
from plating.generator import DocsGenerator
|
|
15
|
+
from plating.models import FunctionInfo, ProviderInfo, ResourceInfo
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"DocsGenerator",
|
|
20
|
+
"FunctionInfo",
|
|
21
|
+
"ProviderInfo",
|
|
22
|
+
"ResourceInfo",
|
|
23
|
+
"main",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# 🥄📚🪄
|
plating/_version.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#
|
|
2
|
+
# _version.py
|
|
3
|
+
#
|
|
4
|
+
"""
|
|
5
|
+
Version handling for plating.
|
|
6
|
+
Uses VERSION file with robust fallback mechanisms.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _find_project_root() -> Path | None:
|
|
13
|
+
"""Find the project root directory by looking for VERSION file."""
|
|
14
|
+
current = Path(__file__).parent
|
|
15
|
+
|
|
16
|
+
# Walk up the directory tree looking for VERSION file
|
|
17
|
+
while current != current.parent: # Stop at filesystem root
|
|
18
|
+
version_file = current / "VERSION"
|
|
19
|
+
if version_file.exists():
|
|
20
|
+
return current
|
|
21
|
+
current = current.parent
|
|
22
|
+
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_version() -> str:
|
|
27
|
+
"""Get the current garnish version.
|
|
28
|
+
|
|
29
|
+
Reads from VERSION file if it exists, otherwise falls back to package metadata,
|
|
30
|
+
then to default development version.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str: The current version string
|
|
34
|
+
"""
|
|
35
|
+
# Try VERSION file first (single source of truth)
|
|
36
|
+
project_root = _find_project_root()
|
|
37
|
+
if project_root:
|
|
38
|
+
version_file = project_root / "VERSION"
|
|
39
|
+
if version_file.exists():
|
|
40
|
+
return version_file.read_text().strip()
|
|
41
|
+
|
|
42
|
+
# Fallback to package metadata
|
|
43
|
+
try:
|
|
44
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
45
|
+
return version("plating")
|
|
46
|
+
except PackageNotFoundError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
# Final fallback
|
|
50
|
+
return "0.0.0-dev"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__version__ = get_version()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/adorner/__init__.py
|
|
3
|
+
#
|
|
4
|
+
"""Adorning system for adding .plating directories to components."""
|
|
5
|
+
|
|
6
|
+
from plating.adorner.api import adorn_components, adorn_missing_components
|
|
7
|
+
from plating.adorner.adorner import PlatingAdorner
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"PlatingAdorner",
|
|
11
|
+
"adorn_components",
|
|
12
|
+
"adorn_missing_components",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 🍲🥄👗🪄
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/adorner/adorner.py
|
|
3
|
+
#
|
|
4
|
+
"""Core adorner implementation."""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
|
|
8
|
+
from pyvider.hub import ComponentDiscovery, hub
|
|
9
|
+
from provide.foundation import logger, pout, perr
|
|
10
|
+
|
|
11
|
+
from plating.adorner.finder import ComponentFinder
|
|
12
|
+
from plating.adorner.templates import TemplateGenerator
|
|
13
|
+
from plating.errors import AdorningError, handle_error
|
|
14
|
+
from plating.plating import PlatingDiscovery
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PlatingAdorner:
|
|
18
|
+
"""Adorns components with .plating directories."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self.plating_discovery = PlatingDiscovery()
|
|
22
|
+
self.template_generator = TemplateGenerator()
|
|
23
|
+
self.component_finder = ComponentFinder()
|
|
24
|
+
|
|
25
|
+
async def adorn_missing(self, component_types: list[str] = None) -> dict[str, int]:
|
|
26
|
+
"""
|
|
27
|
+
Adorn components with missing .plating directories.
|
|
28
|
+
|
|
29
|
+
Returns a dictionary with counts of adorned components by type.
|
|
30
|
+
"""
|
|
31
|
+
# Discover all components via hub
|
|
32
|
+
discovery = ComponentDiscovery(hub)
|
|
33
|
+
await discovery.discover_all()
|
|
34
|
+
components = hub.list_components()
|
|
35
|
+
|
|
36
|
+
# Find existing plating bundles
|
|
37
|
+
existing_bundles = await asyncio.to_thread(
|
|
38
|
+
self.plating_discovery.discover_bundles
|
|
39
|
+
)
|
|
40
|
+
existing_names = {bundle.name for bundle in existing_bundles}
|
|
41
|
+
|
|
42
|
+
# Track adorning results
|
|
43
|
+
adorned = {"resource": 0, "data_source": 0, "function": 0}
|
|
44
|
+
|
|
45
|
+
# Filter by component types if specified
|
|
46
|
+
target_types = component_types or ["resource", "data_source", "function"]
|
|
47
|
+
|
|
48
|
+
# Adorn missing components
|
|
49
|
+
for component_type in target_types:
|
|
50
|
+
if component_type in components:
|
|
51
|
+
for name, component_class in components[component_type].items():
|
|
52
|
+
if name not in existing_names:
|
|
53
|
+
success = await self._adorn_component(
|
|
54
|
+
name, component_type, component_class
|
|
55
|
+
)
|
|
56
|
+
if success:
|
|
57
|
+
adorned[component_type] += 1
|
|
58
|
+
|
|
59
|
+
return adorned
|
|
60
|
+
|
|
61
|
+
async def _adorn_component(
|
|
62
|
+
self, name: str, component_type: str, component_class
|
|
63
|
+
) -> bool:
|
|
64
|
+
"""Adorn a single component with a .plating directory."""
|
|
65
|
+
try:
|
|
66
|
+
# Find the component's source file location
|
|
67
|
+
logger.trace(f"Looking for source file for {name}")
|
|
68
|
+
source_file = await self.component_finder.find_source(component_class)
|
|
69
|
+
if not source_file:
|
|
70
|
+
logger.warning(f"Could not find source file for {name}")
|
|
71
|
+
pout(f"⚠️ Could not find source file for {name}")
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
# Create .plating directory structure
|
|
75
|
+
plating_dir = source_file.parent / f"{source_file.stem}.plating"
|
|
76
|
+
docs_dir = plating_dir / "docs"
|
|
77
|
+
examples_dir = plating_dir / "examples"
|
|
78
|
+
|
|
79
|
+
logger.trace(f"Creating .plating directory at {plating_dir}")
|
|
80
|
+
try:
|
|
81
|
+
await asyncio.to_thread(docs_dir.mkdir, parents=True, exist_ok=True)
|
|
82
|
+
await asyncio.to_thread(examples_dir.mkdir, parents=True, exist_ok=True)
|
|
83
|
+
except OSError as e:
|
|
84
|
+
raise AdorningError(
|
|
85
|
+
name, component_type, f"Failed to create directories: {e}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Generate and write template
|
|
89
|
+
template_content = await self.template_generator.generate_template(
|
|
90
|
+
name, component_type, component_class
|
|
91
|
+
)
|
|
92
|
+
template_file = docs_dir / f"{name}.tmpl.md"
|
|
93
|
+
await asyncio.to_thread(template_file.write_text, template_content)
|
|
94
|
+
|
|
95
|
+
# Generate and write example
|
|
96
|
+
example_content = await self.template_generator.generate_example(
|
|
97
|
+
name, component_type
|
|
98
|
+
)
|
|
99
|
+
example_file = examples_dir / "example.tf"
|
|
100
|
+
await asyncio.to_thread(example_file.write_text, example_content)
|
|
101
|
+
|
|
102
|
+
logger.info(f"Successfully adorned {component_type}: {name}")
|
|
103
|
+
pout(f"✅ Adorned {component_type}: {name}")
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
except AdorningError:
|
|
107
|
+
raise # Re-raise our custom errors
|
|
108
|
+
except Exception as e:
|
|
109
|
+
error = AdorningError(name, component_type, str(e))
|
|
110
|
+
handle_error(error, logger)
|
|
111
|
+
perr(f"❌ Failed to adorn {name}: {e}")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# 🍲🥄👗🪄
|
plating/adorner/api.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/adorner/api.py
|
|
3
|
+
#
|
|
4
|
+
"""Public API for the adorner module."""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
|
|
8
|
+
from plating.adorner.adorner import PlatingAdorner
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Async entry point
|
|
12
|
+
async def adorn_missing_components(component_types: list[str] = None) -> dict[str, int]:
|
|
13
|
+
"""Adorn components with missing .plating directories."""
|
|
14
|
+
adorner = PlatingAdorner()
|
|
15
|
+
return await adorner.adorn_missing(component_types)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Sync entry point
|
|
19
|
+
def adorn_components(component_types: list[str] = None) -> dict[str, int]:
|
|
20
|
+
"""Sync entry point for adorning components."""
|
|
21
|
+
return asyncio.run(adorn_missing_components(component_types))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# 🍲🥄👗🎯🪄
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/adorner/finder.py
|
|
3
|
+
#
|
|
4
|
+
"""Component source file finding utilities."""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ComponentFinder:
|
|
11
|
+
"""Finds source files for components."""
|
|
12
|
+
|
|
13
|
+
async def find_source(self, component_class) -> Path | None:
|
|
14
|
+
"""Find the source file for a component class."""
|
|
15
|
+
try:
|
|
16
|
+
source_file = inspect.getfile(component_class)
|
|
17
|
+
return Path(source_file)
|
|
18
|
+
except Exception:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# 🍲🥄🔍🪄
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/adorner/templates.py
|
|
3
|
+
#
|
|
4
|
+
"""Template generation for adorned components."""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TemplateGenerator:
|
|
8
|
+
"""Generates templates and examples for components."""
|
|
9
|
+
|
|
10
|
+
async def generate_template(
|
|
11
|
+
self, name: str, component_type: str, component_class
|
|
12
|
+
) -> str:
|
|
13
|
+
"""Generate template content based on component type."""
|
|
14
|
+
# Get component description if available
|
|
15
|
+
try:
|
|
16
|
+
doc = component_class.__doc__
|
|
17
|
+
# Check if it's a real docstring (not from Mock or other test objects)
|
|
18
|
+
if doc:
|
|
19
|
+
doc_stripped = doc.strip()
|
|
20
|
+
if not doc_stripped.startswith("Create a new `Mock`"):
|
|
21
|
+
description = doc_stripped.split("\n")[0] # First line only
|
|
22
|
+
else:
|
|
23
|
+
description = (
|
|
24
|
+
f"Terraform {component_type.replace('_', ' ')} for {name}"
|
|
25
|
+
)
|
|
26
|
+
else:
|
|
27
|
+
description = f"Terraform {component_type.replace('_', ' ')} for {name}"
|
|
28
|
+
except AttributeError:
|
|
29
|
+
# No docstring attribute
|
|
30
|
+
description = f"Terraform {component_type.replace('_', ' ')} for {name}"
|
|
31
|
+
|
|
32
|
+
if component_type == "resource":
|
|
33
|
+
return self._resource_template(name, description)
|
|
34
|
+
elif component_type == "data_source":
|
|
35
|
+
return self._data_source_template(name, description)
|
|
36
|
+
elif component_type == "function":
|
|
37
|
+
return self._function_template(name, description)
|
|
38
|
+
else:
|
|
39
|
+
return self._generic_template(name, description, component_type)
|
|
40
|
+
|
|
41
|
+
async def generate_example(self, name: str, component_type: str) -> str:
|
|
42
|
+
"""Generate example Terraform content."""
|
|
43
|
+
if component_type == "resource":
|
|
44
|
+
return self._resource_example(name)
|
|
45
|
+
elif component_type == "data_source":
|
|
46
|
+
return self._data_source_example(name)
|
|
47
|
+
elif component_type == "function":
|
|
48
|
+
return self._function_example(name)
|
|
49
|
+
else:
|
|
50
|
+
return self._generic_example(name)
|
|
51
|
+
|
|
52
|
+
def _resource_template(self, name: str, description: str) -> str:
|
|
53
|
+
"""Generate resource template content."""
|
|
54
|
+
return f"""---
|
|
55
|
+
page_title: "Resource: {name}"
|
|
56
|
+
description: |-
|
|
57
|
+
{description}
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
# {name} (Resource)
|
|
61
|
+
|
|
62
|
+
{description}
|
|
63
|
+
|
|
64
|
+
## Example Usage
|
|
65
|
+
|
|
66
|
+
{{{{ example("example") }}}}
|
|
67
|
+
|
|
68
|
+
## Argument Reference
|
|
69
|
+
|
|
70
|
+
{{{{ schema() }}}}
|
|
71
|
+
|
|
72
|
+
## Import
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
terraform import {name}.example <id>
|
|
76
|
+
```
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def _data_source_template(self, name: str, description: str) -> str:
|
|
80
|
+
"""Generate data source template content."""
|
|
81
|
+
return f"""---
|
|
82
|
+
page_title: "Data Source: {name}"
|
|
83
|
+
description: |-
|
|
84
|
+
{description}
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
# {name} (Data Source)
|
|
88
|
+
|
|
89
|
+
{description}
|
|
90
|
+
|
|
91
|
+
## Example Usage
|
|
92
|
+
|
|
93
|
+
{{{{ example("example") }}}}
|
|
94
|
+
|
|
95
|
+
## Argument Reference
|
|
96
|
+
|
|
97
|
+
{{{{ schema() }}}}
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def _function_template(self, name: str, description: str) -> str:
|
|
101
|
+
"""Generate function template content."""
|
|
102
|
+
return f"""---
|
|
103
|
+
page_title: "Function: {name}"
|
|
104
|
+
description: |-
|
|
105
|
+
{description}
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
# {name} (Function)
|
|
109
|
+
|
|
110
|
+
{description}
|
|
111
|
+
|
|
112
|
+
## Example Usage
|
|
113
|
+
|
|
114
|
+
{{{{ example("example") }}}}
|
|
115
|
+
|
|
116
|
+
## Signature
|
|
117
|
+
|
|
118
|
+
`{{{{ signature_markdown }}}}`
|
|
119
|
+
|
|
120
|
+
## Arguments
|
|
121
|
+
|
|
122
|
+
{{{{ arguments_markdown }}}}
|
|
123
|
+
|
|
124
|
+
{{% if has_variadic %}}
|
|
125
|
+
## Variadic Arguments
|
|
126
|
+
|
|
127
|
+
{{{{ variadic_argument_markdown }}}}
|
|
128
|
+
{{% endif %}}
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def _generic_template(
|
|
132
|
+
self, name: str, description: str, component_type: str
|
|
133
|
+
) -> str:
|
|
134
|
+
"""Generate generic template content."""
|
|
135
|
+
return f"""---
|
|
136
|
+
page_title: "{component_type.title()}: {name}"
|
|
137
|
+
description: |-
|
|
138
|
+
{description}
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
# {name} ({component_type.title()})
|
|
142
|
+
|
|
143
|
+
{description}
|
|
144
|
+
|
|
145
|
+
## Example Usage
|
|
146
|
+
|
|
147
|
+
{{{{ example("example") }}}}
|
|
148
|
+
|
|
149
|
+
## Schema
|
|
150
|
+
|
|
151
|
+
{{{{ schema() }}}}
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def _resource_example(self, name: str) -> str:
|
|
155
|
+
"""Generate resource example."""
|
|
156
|
+
return f'''resource "{name}" "example" {{
|
|
157
|
+
# Configuration options here
|
|
158
|
+
}}
|
|
159
|
+
|
|
160
|
+
output "example_id" {{
|
|
161
|
+
description = "The ID of the {name} resource"
|
|
162
|
+
value = {name}.example.id
|
|
163
|
+
}}
|
|
164
|
+
'''
|
|
165
|
+
|
|
166
|
+
def _data_source_example(self, name: str) -> str:
|
|
167
|
+
"""Generate data source example."""
|
|
168
|
+
return f'''data "{name}" "example" {{
|
|
169
|
+
# Configuration options here
|
|
170
|
+
}}
|
|
171
|
+
|
|
172
|
+
output "example_data" {{
|
|
173
|
+
description = "Data from {name}"
|
|
174
|
+
value = data.{name}.example
|
|
175
|
+
}}
|
|
176
|
+
'''
|
|
177
|
+
|
|
178
|
+
def _function_example(self, name: str) -> str:
|
|
179
|
+
"""Generate function example."""
|
|
180
|
+
return f"""locals {{
|
|
181
|
+
example_result = {name}(
|
|
182
|
+
# Function arguments here
|
|
183
|
+
)
|
|
184
|
+
}}
|
|
185
|
+
|
|
186
|
+
output "function_result" {{
|
|
187
|
+
description = "Result of {name} function"
|
|
188
|
+
value = local.example_result
|
|
189
|
+
}}
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def _generic_example(self, name: str) -> str:
|
|
193
|
+
"""Generate generic example."""
|
|
194
|
+
return f"""# Example usage for {name}
|
|
195
|
+
# Add your Terraform configuration here
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# 🍲🥄👗📝🪄
|