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/plater.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/plater.py
|
|
3
|
+
#
|
|
4
|
+
"""Garnish documentation plating system."""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from jinja2 import DictLoader, Environment, select_autoescape
|
|
9
|
+
from provide.foundation import logger
|
|
10
|
+
|
|
11
|
+
from plating.errors import PlatingRenderError, TemplateError, handle_error
|
|
12
|
+
from plating.plating import PlatingBundle, PlatingDiscovery
|
|
13
|
+
from plating.schema import SchemaProcessor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PlatingPlater:
|
|
17
|
+
"""Documentation plater using .plating bundles."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
bundles: list[PlatingBundle] | None = None,
|
|
22
|
+
schema_processor: SchemaProcessor | None = None,
|
|
23
|
+
):
|
|
24
|
+
"""Initialize plater with bundles and optional schema processor.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
bundles: List of PlatingBundle objects to render
|
|
28
|
+
schema_processor: Optional schema processor for schema extraction
|
|
29
|
+
"""
|
|
30
|
+
self.bundles = bundles or []
|
|
31
|
+
self.schema_processor = schema_processor
|
|
32
|
+
self.provider_schema = None
|
|
33
|
+
|
|
34
|
+
if self.schema_processor:
|
|
35
|
+
try:
|
|
36
|
+
self.provider_schema = self.schema_processor.extract_provider_schema()
|
|
37
|
+
except Exception as e:
|
|
38
|
+
handle_error(e, logger)
|
|
39
|
+
logger.warning(f"Failed to extract provider schema: {e}")
|
|
40
|
+
|
|
41
|
+
def plate(self, output_dir: Path, force: bool = False) -> None:
|
|
42
|
+
"""Plate all bundles to the output directory.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
output_dir: Directory to write plated documentation
|
|
46
|
+
force: Force plating even if output exists
|
|
47
|
+
"""
|
|
48
|
+
output_dir = Path(output_dir)
|
|
49
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
|
|
51
|
+
for bundle in self.bundles:
|
|
52
|
+
try:
|
|
53
|
+
self._plate_bundle(bundle, output_dir, force)
|
|
54
|
+
except PlatingRenderError:
|
|
55
|
+
raise # Re-raise our custom errors
|
|
56
|
+
except Exception as e:
|
|
57
|
+
error = PlatingRenderError(bundle.name, str(e))
|
|
58
|
+
handle_error(error, logger)
|
|
59
|
+
logger.error(f"Failed to plate bundle {bundle.name}: {e}")
|
|
60
|
+
|
|
61
|
+
def _plate_bundle(
|
|
62
|
+
self, bundle: PlatingBundle, output_dir: Path, force: bool
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Plate a single bundle.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
bundle: The PlatingBundle to render
|
|
68
|
+
output_dir: Directory to write output
|
|
69
|
+
force: Force overwrite existing files
|
|
70
|
+
"""
|
|
71
|
+
# Load bundle assets
|
|
72
|
+
logger.trace(f"Loading assets for bundle {bundle.name}")
|
|
73
|
+
template_content = bundle.load_main_template()
|
|
74
|
+
if not template_content:
|
|
75
|
+
logger.debug(f"No template found for {bundle.name}, skipping")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
examples = bundle.load_examples()
|
|
79
|
+
partials = bundle.load_partials()
|
|
80
|
+
|
|
81
|
+
# Create plating context
|
|
82
|
+
context = _create_plating_context(
|
|
83
|
+
bundle,
|
|
84
|
+
self._get_schema_for_component(bundle),
|
|
85
|
+
self.schema_processor.provider_name
|
|
86
|
+
if self.schema_processor
|
|
87
|
+
else "provider",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Add examples to context
|
|
91
|
+
context["examples"] = examples
|
|
92
|
+
|
|
93
|
+
# Plate template
|
|
94
|
+
try:
|
|
95
|
+
plated = self._plate_template(template_content, context, partials)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.error(f"Template rendering failed for {bundle.name}: {e}")
|
|
98
|
+
return # Skip this bundle on error
|
|
99
|
+
|
|
100
|
+
# Determine output path
|
|
101
|
+
subdir = _get_output_subdir(bundle.component_type)
|
|
102
|
+
output_path = output_dir / subdir / f"{bundle.name}.md"
|
|
103
|
+
|
|
104
|
+
# Check if file exists and force flag
|
|
105
|
+
if output_path.exists() and not force:
|
|
106
|
+
logger.debug(
|
|
107
|
+
f"Output file {output_path} exists, skipping (use force=True to overwrite)"
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
# Write output
|
|
112
|
+
try:
|
|
113
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
output_path.write_text(plated)
|
|
115
|
+
logger.info(f"Successfully plated {bundle.name} to {output_path}")
|
|
116
|
+
except OSError as e:
|
|
117
|
+
raise PlatingRenderError(bundle.name, f"Failed to write output file: {e}")
|
|
118
|
+
|
|
119
|
+
def _get_schema_for_component(self, bundle: PlatingBundle) -> dict | None:
|
|
120
|
+
"""Get schema for a component from the provider schema.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
bundle: The bundle to get schema for
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Component schema dict or None
|
|
127
|
+
"""
|
|
128
|
+
if not self.provider_schema:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# Try to find the component schema
|
|
132
|
+
provider_schemas = self.provider_schema.get("provider_schemas", {})
|
|
133
|
+
for provider_key, provider_data in provider_schemas.items():
|
|
134
|
+
# Check resources
|
|
135
|
+
if bundle.component_type == "resource":
|
|
136
|
+
schemas = provider_data.get("resource_schemas", {})
|
|
137
|
+
if bundle.name in schemas:
|
|
138
|
+
return schemas[bundle.name]
|
|
139
|
+
if f"pyvider_{bundle.name}" in schemas:
|
|
140
|
+
return schemas[f"pyvider_{bundle.name}"]
|
|
141
|
+
|
|
142
|
+
# Check data sources
|
|
143
|
+
elif bundle.component_type == "data_source":
|
|
144
|
+
schemas = provider_data.get("data_source_schemas", {})
|
|
145
|
+
if bundle.name in schemas:
|
|
146
|
+
return schemas[bundle.name]
|
|
147
|
+
if f"pyvider_{bundle.name}" in schemas:
|
|
148
|
+
return schemas[f"pyvider_{bundle.name}"]
|
|
149
|
+
|
|
150
|
+
# Check functions
|
|
151
|
+
elif bundle.component_type == "function":
|
|
152
|
+
functions = provider_data.get("functions", {})
|
|
153
|
+
if bundle.name in functions:
|
|
154
|
+
return functions[bundle.name]
|
|
155
|
+
if f"pyvider_{bundle.name}" in functions:
|
|
156
|
+
return functions[f"pyvider_{bundle.name}"]
|
|
157
|
+
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def _plate_template(
|
|
161
|
+
self, template_content: str, context: dict, partials: dict[str, str]
|
|
162
|
+
) -> str:
|
|
163
|
+
"""Plate a Jinja2 template with context.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
template_content: The template string
|
|
167
|
+
context: Rendering context dictionary
|
|
168
|
+
partials: Partial templates dictionary
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Plated template string
|
|
172
|
+
"""
|
|
173
|
+
# Set up Jinja2 environment
|
|
174
|
+
templates = {"main.tmpl": template_content}
|
|
175
|
+
templates.update(partials)
|
|
176
|
+
|
|
177
|
+
env = Environment(
|
|
178
|
+
loader=DictLoader(templates),
|
|
179
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Add custom template functions
|
|
183
|
+
env.globals["schema"] = lambda: context.get("schema_markdown", "")
|
|
184
|
+
env.globals["example"] = lambda name: _format_example(
|
|
185
|
+
context.get("examples", {}).get(name, "")
|
|
186
|
+
)
|
|
187
|
+
env.globals["include"] = lambda filename: partials.get(filename, "")
|
|
188
|
+
|
|
189
|
+
# Plate template
|
|
190
|
+
template = env.get_template("main.tmpl")
|
|
191
|
+
return template.render(**context)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _create_plating_context(
|
|
195
|
+
bundle: PlatingBundle, schema: dict | None, provider_name: str
|
|
196
|
+
) -> dict:
|
|
197
|
+
"""Create plating context for a bundle.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
bundle: The PlatingBundle
|
|
201
|
+
schema: Component schema dict or None
|
|
202
|
+
provider_name: Name of the provider
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Context dictionary for template plating
|
|
206
|
+
"""
|
|
207
|
+
context = {
|
|
208
|
+
"name": bundle.name,
|
|
209
|
+
"type": _format_component_type(bundle.component_type),
|
|
210
|
+
"provider_name": provider_name,
|
|
211
|
+
"component_type": bundle.component_type,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if schema:
|
|
215
|
+
context["description"] = schema.get("description", "")
|
|
216
|
+
context["schema_markdown"] = _plate_schema_markdown(schema)
|
|
217
|
+
|
|
218
|
+
# Add function-specific fields
|
|
219
|
+
if bundle.component_type == "function" and "signature" in schema:
|
|
220
|
+
context["signature"] = _format_function_signature(schema)
|
|
221
|
+
context["arguments"] = _format_function_arguments(schema)
|
|
222
|
+
|
|
223
|
+
return context
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _format_component_type(component_type: str) -> str:
|
|
227
|
+
"""Format component type for display.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
component_type: Raw component type
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Formatted component type
|
|
234
|
+
"""
|
|
235
|
+
return {
|
|
236
|
+
"resource": "Resource",
|
|
237
|
+
"data_source": "Data Source",
|
|
238
|
+
"function": "Function",
|
|
239
|
+
}.get(component_type, component_type.title())
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _get_output_subdir(component_type: str) -> str:
|
|
243
|
+
"""Get output subdirectory for component type.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
component_type: Component type
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Output subdirectory name
|
|
250
|
+
"""
|
|
251
|
+
return {
|
|
252
|
+
"resource": "resources",
|
|
253
|
+
"data_source": "data_sources",
|
|
254
|
+
"function": "functions",
|
|
255
|
+
}.get(component_type, "resources")
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _format_example(example_code: str) -> str:
|
|
259
|
+
"""Format example code for display.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
example_code: Raw example code
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Formatted example with code block
|
|
266
|
+
"""
|
|
267
|
+
if not example_code:
|
|
268
|
+
return ""
|
|
269
|
+
return f"```terraform\n{example_code}\n```"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _plate_schema_markdown(schema: dict) -> str:
|
|
273
|
+
"""Plate schema to markdown format.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
schema: Schema dictionary
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Markdown formatted schema
|
|
280
|
+
"""
|
|
281
|
+
lines = ["## Schema", ""]
|
|
282
|
+
|
|
283
|
+
block = schema.get("block", {})
|
|
284
|
+
attributes = block.get("attributes", {})
|
|
285
|
+
|
|
286
|
+
# Separate attributes by type
|
|
287
|
+
required_attrs = []
|
|
288
|
+
optional_attrs = []
|
|
289
|
+
computed_attrs = []
|
|
290
|
+
|
|
291
|
+
for attr_name, attr_def in attributes.items():
|
|
292
|
+
attr_type = _format_type_string(attr_def.get("type"))
|
|
293
|
+
description = attr_def.get("description", "")
|
|
294
|
+
|
|
295
|
+
if attr_def.get("required"):
|
|
296
|
+
required_attrs.append((attr_name, attr_type, description))
|
|
297
|
+
elif attr_def.get("computed") and not attr_def.get("optional"):
|
|
298
|
+
computed_attrs.append((attr_name, attr_type, description))
|
|
299
|
+
else:
|
|
300
|
+
optional_attrs.append((attr_name, attr_type, description))
|
|
301
|
+
|
|
302
|
+
# Format sections
|
|
303
|
+
if required_attrs:
|
|
304
|
+
lines.extend(["### Required", ""])
|
|
305
|
+
for name, type_str, desc in required_attrs:
|
|
306
|
+
lines.append(f"- `{name}` ({type_str}) - {desc}")
|
|
307
|
+
lines.append("")
|
|
308
|
+
|
|
309
|
+
if optional_attrs:
|
|
310
|
+
lines.extend(["### Optional", ""])
|
|
311
|
+
for name, type_str, desc in optional_attrs:
|
|
312
|
+
lines.append(f"- `{name}` ({type_str}) - {desc}")
|
|
313
|
+
lines.append("")
|
|
314
|
+
|
|
315
|
+
if computed_attrs:
|
|
316
|
+
lines.extend(["### Read-Only", ""])
|
|
317
|
+
for name, type_str, desc in computed_attrs:
|
|
318
|
+
lines.append(f"- `{name}` ({type_str}) - {desc}")
|
|
319
|
+
lines.append("")
|
|
320
|
+
|
|
321
|
+
# Handle nested blocks
|
|
322
|
+
blocks = block.get("block_types", {})
|
|
323
|
+
if blocks:
|
|
324
|
+
lines.extend(["### Blocks", ""])
|
|
325
|
+
for block_name, block_def in blocks.items():
|
|
326
|
+
max_items = block_def.get("max_items", 0)
|
|
327
|
+
if max_items == 1:
|
|
328
|
+
lines.append(f"- `{block_name}` (Optional)")
|
|
329
|
+
else:
|
|
330
|
+
lines.append(f"- `{block_name}` (Optional, List)")
|
|
331
|
+
lines.append("")
|
|
332
|
+
|
|
333
|
+
# Return empty string if no content was generated
|
|
334
|
+
if len(lines) == 2: # Just "## Schema" and empty line
|
|
335
|
+
return ""
|
|
336
|
+
|
|
337
|
+
return "\n".join(lines)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _format_type_string(type_info) -> str:
|
|
341
|
+
"""Format type information to human-readable string.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
type_info: Type information (string, list, or dict)
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
Formatted type string
|
|
348
|
+
"""
|
|
349
|
+
if not type_info:
|
|
350
|
+
return "Dynamic"
|
|
351
|
+
|
|
352
|
+
if isinstance(type_info, str):
|
|
353
|
+
return type_info.title()
|
|
354
|
+
|
|
355
|
+
if isinstance(type_info, list) and len(type_info) >= 2:
|
|
356
|
+
container_type = type_info[0]
|
|
357
|
+
element_type = type_info[1]
|
|
358
|
+
|
|
359
|
+
if container_type == "list":
|
|
360
|
+
return f"List of {_format_type_string(element_type)}"
|
|
361
|
+
elif container_type == "set":
|
|
362
|
+
return f"Set of {_format_type_string(element_type)}"
|
|
363
|
+
elif container_type == "map":
|
|
364
|
+
return f"Map of {_format_type_string(element_type)}"
|
|
365
|
+
elif container_type == "object":
|
|
366
|
+
if isinstance(element_type, dict):
|
|
367
|
+
attrs = ", ".join(
|
|
368
|
+
f"{k}: {_format_type_string(v)}" for k, v in element_type.items()
|
|
369
|
+
)
|
|
370
|
+
return f"Object({attrs})"
|
|
371
|
+
return "Object"
|
|
372
|
+
|
|
373
|
+
return "Dynamic"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _format_function_signature(schema: dict) -> str:
|
|
377
|
+
"""Format function signature from schema.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
schema: Function schema
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Formatted function signature
|
|
384
|
+
"""
|
|
385
|
+
signature = schema.get("signature", {})
|
|
386
|
+
params = []
|
|
387
|
+
|
|
388
|
+
# Parameters
|
|
389
|
+
for param in signature.get("parameters", []):
|
|
390
|
+
param_name = param.get("name", "arg")
|
|
391
|
+
param_type = param.get("type", "any")
|
|
392
|
+
params.append(f"{param_name}: {param_type}")
|
|
393
|
+
|
|
394
|
+
# Variadic parameter
|
|
395
|
+
if "variadic_parameter" in signature:
|
|
396
|
+
variadic = signature["variadic_parameter"]
|
|
397
|
+
variadic_name = variadic.get("name", "args")
|
|
398
|
+
variadic_type = variadic.get("type", "any")
|
|
399
|
+
params.append(f"...{variadic_name}: {variadic_type}")
|
|
400
|
+
|
|
401
|
+
# Return type
|
|
402
|
+
return_type = signature.get("return_type", "any")
|
|
403
|
+
param_str = ", ".join(params)
|
|
404
|
+
|
|
405
|
+
return f"({param_str}) -> {return_type}"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _format_function_arguments(schema: dict) -> str:
|
|
409
|
+
"""Format function arguments from schema.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
schema: Function schema
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
Formatted arguments list
|
|
416
|
+
"""
|
|
417
|
+
signature = schema.get("signature", {})
|
|
418
|
+
lines = []
|
|
419
|
+
|
|
420
|
+
# Parameters
|
|
421
|
+
for param in signature.get("parameters", []):
|
|
422
|
+
param_name = param.get("name", "arg")
|
|
423
|
+
param_type = param.get("type", "any")
|
|
424
|
+
description = param.get("description", "")
|
|
425
|
+
lines.append(f"- `{param_name}` ({param_type}) - {description}")
|
|
426
|
+
|
|
427
|
+
# Variadic parameter
|
|
428
|
+
if "variadic_parameter" in signature:
|
|
429
|
+
variadic = signature["variadic_parameter"]
|
|
430
|
+
variadic_name = variadic.get("name", "args")
|
|
431
|
+
variadic_type = variadic.get("type", "any")
|
|
432
|
+
description = variadic.get("description", "")
|
|
433
|
+
lines.append(f"- `...{variadic_name}` ({variadic_type}) - {description}")
|
|
434
|
+
|
|
435
|
+
return "\n".join(lines)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def generate_docs(
|
|
439
|
+
output_dir: Path | str = "docs",
|
|
440
|
+
provider_name: str | None = None,
|
|
441
|
+
package_name: str = "pyvider.components",
|
|
442
|
+
component_type: str | None = None,
|
|
443
|
+
force: bool = False,
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Generate documentation for all discovered plating bundles.
|
|
446
|
+
|
|
447
|
+
This is the main entry point for documentation generation.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
output_dir: Directory to write documentation
|
|
451
|
+
provider_name: Optional provider name for schema extraction
|
|
452
|
+
package_name: Package to search for plating bundles
|
|
453
|
+
component_type: Optional filter for component type
|
|
454
|
+
force: Force overwrite existing files
|
|
455
|
+
"""
|
|
456
|
+
# Discover bundles
|
|
457
|
+
discovery = PlatingDiscovery(package_name)
|
|
458
|
+
bundles = discovery.discover_bundles(component_type)
|
|
459
|
+
|
|
460
|
+
if not bundles:
|
|
461
|
+
logger.warning(f"No plating bundles found in {package_name}")
|
|
462
|
+
return
|
|
463
|
+
|
|
464
|
+
logger.info(f"Found {len(bundles)} plating bundles")
|
|
465
|
+
|
|
466
|
+
# Initialize schema processor if provider name given
|
|
467
|
+
schema_processor = None
|
|
468
|
+
if provider_name:
|
|
469
|
+
try:
|
|
470
|
+
# Create mock generator for schema processor
|
|
471
|
+
mock_generator = type(
|
|
472
|
+
"MockGenerator",
|
|
473
|
+
(),
|
|
474
|
+
{"provider_name": provider_name, "provider_dir": Path.cwd()},
|
|
475
|
+
)()
|
|
476
|
+
schema_processor = SchemaProcessor(mock_generator)
|
|
477
|
+
except Exception as e:
|
|
478
|
+
logger.warning(f"Failed to initialize schema processor: {e}")
|
|
479
|
+
|
|
480
|
+
# Create renderer and plate
|
|
481
|
+
plater = PlatingPlater(bundles, schema_processor)
|
|
482
|
+
plater.plate(Path(output_dir), force)
|
|
483
|
+
|
|
484
|
+
logger.info(f"Documentation generated in {output_dir}")
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
# 🍲🥄📄🪄
|
plating/plating.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#
|
|
2
|
+
# plating/garnish.py
|
|
3
|
+
#
|
|
4
|
+
"""Garnish bundle discovery and management."""
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import attrs
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@attrs.define
|
|
13
|
+
class PlatingBundle:
|
|
14
|
+
"""Represents a single .plating bundle with its assets."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
plating_dir: Path
|
|
18
|
+
component_type: str # "resource", "data_source", "function"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def docs_dir(self) -> Path:
|
|
22
|
+
"""Directory containing documentation templates and partials."""
|
|
23
|
+
return self.plating_dir / "docs"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def examples_dir(self) -> Path:
|
|
27
|
+
"""Directory containing example Terraform files."""
|
|
28
|
+
return self.plating_dir / "examples"
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def fixtures_dir(self) -> Path:
|
|
32
|
+
"""Directory containing fixture files for tests (inside examples dir)."""
|
|
33
|
+
return self.examples_dir / "fixtures"
|
|
34
|
+
|
|
35
|
+
def load_main_template(self) -> str | None:
|
|
36
|
+
"""Load the main template file for this component."""
|
|
37
|
+
# Main template is typically <component_name>.tmpl.md
|
|
38
|
+
template_file = self.docs_dir / f"{self.name}.tmpl.md"
|
|
39
|
+
|
|
40
|
+
if not template_file.exists():
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
return template_file.read_text(encoding="utf-8")
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
def load_examples(self) -> dict[str, str]:
|
|
49
|
+
"""Load all example files as a dictionary."""
|
|
50
|
+
examples = {}
|
|
51
|
+
|
|
52
|
+
if not self.examples_dir.exists():
|
|
53
|
+
return examples
|
|
54
|
+
|
|
55
|
+
for example_file in self.examples_dir.glob("*.tf"):
|
|
56
|
+
try:
|
|
57
|
+
examples[example_file.stem] = example_file.read_text(encoding="utf-8")
|
|
58
|
+
except Exception:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
return examples
|
|
62
|
+
|
|
63
|
+
def load_partials(self) -> dict[str, str]:
|
|
64
|
+
"""Load all partial files from docs directory.
|
|
65
|
+
|
|
66
|
+
Partials are files starting with underscore (_) in the docs directory.
|
|
67
|
+
"""
|
|
68
|
+
partials = {}
|
|
69
|
+
|
|
70
|
+
if not self.docs_dir.exists():
|
|
71
|
+
return partials
|
|
72
|
+
|
|
73
|
+
# Load only files starting with underscore (partial convention)
|
|
74
|
+
for partial_file in self.docs_dir.glob("_*"):
|
|
75
|
+
if partial_file.is_file():
|
|
76
|
+
try:
|
|
77
|
+
partials[partial_file.name] = partial_file.read_text(
|
|
78
|
+
encoding="utf-8"
|
|
79
|
+
)
|
|
80
|
+
except Exception:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
return partials
|
|
84
|
+
|
|
85
|
+
def load_fixtures(self) -> dict[str, str]:
|
|
86
|
+
"""Load all fixture files from fixtures directory."""
|
|
87
|
+
fixtures = {}
|
|
88
|
+
|
|
89
|
+
if not self.fixtures_dir.exists():
|
|
90
|
+
return fixtures
|
|
91
|
+
|
|
92
|
+
for fixture_file in self.fixtures_dir.rglob("*"):
|
|
93
|
+
if fixture_file.is_file():
|
|
94
|
+
try:
|
|
95
|
+
# Use relative path from fixtures dir as key
|
|
96
|
+
rel_path = fixture_file.relative_to(self.fixtures_dir)
|
|
97
|
+
fixtures[str(rel_path)] = fixture_file.read_text(encoding="utf-8")
|
|
98
|
+
except Exception:
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
return fixtures
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class PlatingDiscovery:
|
|
105
|
+
"""Discovers .plating bundles from installed packages."""
|
|
106
|
+
|
|
107
|
+
def __init__(self, package_name: str = "pyvider.components"):
|
|
108
|
+
self.package_name = package_name
|
|
109
|
+
|
|
110
|
+
def discover_bundles(
|
|
111
|
+
self, component_type: str | None = None
|
|
112
|
+
) -> list[PlatingBundle]:
|
|
113
|
+
"""Discover all .plating bundles from the installed package."""
|
|
114
|
+
bundles = []
|
|
115
|
+
|
|
116
|
+
# Find the package location
|
|
117
|
+
try:
|
|
118
|
+
spec = importlib.util.find_spec(self.package_name)
|
|
119
|
+
if not spec or not spec.origin:
|
|
120
|
+
return bundles
|
|
121
|
+
except (ModuleNotFoundError, ValueError):
|
|
122
|
+
# Package doesn't exist or invalid package name
|
|
123
|
+
return bundles
|
|
124
|
+
|
|
125
|
+
package_path = Path(spec.origin).parent
|
|
126
|
+
|
|
127
|
+
# Search for .plating directories
|
|
128
|
+
for plating_dir in package_path.rglob("*.plating"):
|
|
129
|
+
if not plating_dir.is_dir():
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# Skip hidden directories
|
|
133
|
+
if plating_dir.name.startswith("."):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Determine component type from path
|
|
137
|
+
bundle_component_type = self._determine_component_type(plating_dir)
|
|
138
|
+
if component_type and bundle_component_type != component_type:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# Check if this is a multi-component bundle
|
|
142
|
+
sub_component_bundles = self._discover_sub_components(
|
|
143
|
+
plating_dir, bundle_component_type
|
|
144
|
+
)
|
|
145
|
+
if sub_component_bundles:
|
|
146
|
+
# Multi-component bundle - use individual components
|
|
147
|
+
bundles.extend(sub_component_bundles)
|
|
148
|
+
else:
|
|
149
|
+
# Single component bundle
|
|
150
|
+
component_name = plating_dir.name.replace(".plating", "")
|
|
151
|
+
|
|
152
|
+
bundle = PlatingBundle(
|
|
153
|
+
name=component_name,
|
|
154
|
+
plating_dir=plating_dir,
|
|
155
|
+
component_type=bundle_component_type,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
bundles.append(bundle)
|
|
159
|
+
|
|
160
|
+
return bundles
|
|
161
|
+
|
|
162
|
+
def _discover_sub_components(
|
|
163
|
+
self, plating_dir: Path, component_type: str
|
|
164
|
+
) -> list[PlatingBundle]:
|
|
165
|
+
"""Discover individual components within a multi-component .plating bundle."""
|
|
166
|
+
sub_bundles = []
|
|
167
|
+
|
|
168
|
+
# Look for subdirectories that contain docs/ and examples/ folders
|
|
169
|
+
for item in plating_dir.iterdir():
|
|
170
|
+
if not item.is_dir():
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Check if this looks like a component directory
|
|
174
|
+
docs_dir = item / "docs"
|
|
175
|
+
if docs_dir.exists() and docs_dir.is_dir():
|
|
176
|
+
# Determine component type from subdirectory name
|
|
177
|
+
sub_component_type = item.name
|
|
178
|
+
if sub_component_type not in ["resource", "data_source", "function"]:
|
|
179
|
+
# Fall back to parent component type if not a recognized type
|
|
180
|
+
sub_component_type = component_type
|
|
181
|
+
|
|
182
|
+
# This appears to be an individual component
|
|
183
|
+
bundle = PlatingBundle(
|
|
184
|
+
name=item.name, # Use the directory name as component name
|
|
185
|
+
plating_dir=item, # Point to the individual component directory
|
|
186
|
+
component_type=sub_component_type,
|
|
187
|
+
)
|
|
188
|
+
sub_bundles.append(bundle)
|
|
189
|
+
|
|
190
|
+
return sub_bundles
|
|
191
|
+
|
|
192
|
+
def _determine_component_type(self, plating_dir: Path) -> str:
|
|
193
|
+
"""Determine component type from the .plating directory path."""
|
|
194
|
+
path_parts = plating_dir.parts
|
|
195
|
+
|
|
196
|
+
if "resources" in path_parts:
|
|
197
|
+
return "resource"
|
|
198
|
+
elif "data_sources" in path_parts:
|
|
199
|
+
return "data_source"
|
|
200
|
+
elif "functions" in path_parts:
|
|
201
|
+
return "function"
|
|
202
|
+
else:
|
|
203
|
+
# Default to resource if unclear
|
|
204
|
+
return "resource"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# 🍲🥄📄🪄
|