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 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
+ # 🍲🥄👗📝🪄