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/schema.py ADDED
@@ -0,0 +1,502 @@
1
+ #
2
+ # plating/schema.py
3
+ #
4
+ """Schema extraction and processing for documentation generation."""
5
+
6
+ import json
7
+ from pathlib import Path
8
+ import shutil
9
+ import subprocess
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import attrs
13
+ from pyvider.hub import ComponentDiscovery, hub
14
+ from provide.foundation import logger, pout, perr
15
+ from provide.foundation.process import run_command, ProcessError
16
+
17
+ from plating.config import get_config
18
+ from plating.errors import SchemaError
19
+ from plating.models import FunctionInfo, ProviderInfo, ResourceInfo
20
+
21
+ if TYPE_CHECKING:
22
+ from .generator import DocsGenerator
23
+
24
+
25
+ class SchemaProcessor:
26
+ """Handles schema extraction and processing."""
27
+
28
+ def __init__(self, generator: "DocsGenerator"):
29
+ self.generator = generator
30
+
31
+ def extract_provider_schema(self) -> dict[str, Any]:
32
+ """Extract provider schema using Pyvider's component discovery."""
33
+ import asyncio
34
+
35
+ return asyncio.run(self._extract_schema_via_discovery())
36
+
37
+ async def _extract_schema_via_discovery(self) -> dict[str, Any]:
38
+ """Extract schema by discovering components and inspecting their schemas."""
39
+ logger.info("Discovering components via Pyvider hub...")
40
+ pout("🔍 Discovering components via Pyvider hub...")
41
+
42
+ try:
43
+ discovery = ComponentDiscovery(hub)
44
+ await discovery.discover_all()
45
+ except Exception as e:
46
+ raise SchemaError(
47
+ self.generator.provider_name, f"Component discovery failed: {e}"
48
+ )
49
+
50
+ components = hub.list_components()
51
+
52
+ provider_schema = {
53
+ "provider_schemas": {
54
+ f"registry.terraform.io/local/providers/{self.generator.provider_name}": {
55
+ "provider": self._get_provider_schema(
56
+ components.get("provider", {})
57
+ ),
58
+ "resource_schemas": self._get_component_schemas(
59
+ components.get("resource", {})
60
+ ),
61
+ "data_source_schemas": self._get_component_schemas(
62
+ components.get("data_source", {})
63
+ ),
64
+ "functions": self._get_function_schemas(
65
+ components.get("function", {})
66
+ ),
67
+ }
68
+ }
69
+ }
70
+ return provider_schema
71
+
72
+ def _get_provider_schema(self, providers: dict[str, Any]) -> dict[str, Any]:
73
+ if not providers:
74
+ return {"block": {"attributes": {}}}
75
+
76
+ try:
77
+ provider_component = next(iter(providers.values()))
78
+ if hasattr(provider_component, "get_schema"):
79
+ schema = provider_component.get_schema()
80
+ return attrs.asdict(schema)
81
+ except Exception as e:
82
+ logger.warning(f"Failed to get provider schema: {e}")
83
+
84
+ return {"block": {"attributes": {}}}
85
+
86
+ def _get_component_schemas(self, components: dict[str, Any]) -> dict[str, Any]:
87
+ """Get schemas for resources or data sources."""
88
+ schemas = {}
89
+ for name, component in components.items():
90
+ if hasattr(component, "get_schema"):
91
+ schema = component.get_schema()
92
+ schemas[name] = attrs.asdict(schema)
93
+ elif hasattr(component, "__pyvider_schema__"):
94
+ schema_attr = component.__pyvider_schema__
95
+ schemas[name] = schema_attr
96
+ return schemas
97
+
98
+ def _get_function_schemas(self, functions: dict[str, Any]) -> dict[str, Any]:
99
+ """Get schemas for functions."""
100
+ schemas = {}
101
+ for name, func in functions.items():
102
+ if hasattr(func, "get_schema"):
103
+ schema = func.get_schema()
104
+ schemas[name] = attrs.asdict(schema)
105
+ elif hasattr(func, "__pyvider_schema__"):
106
+ schemas[name] = func.__pyvider_schema__
107
+ return schemas
108
+
109
+ def _extract_schema_via_terraform(self) -> dict[str, Any]:
110
+ """Fallback: Extract schema by building provider and using Terraform CLI."""
111
+ config = get_config()
112
+ tf_binary = config.terraform_binary or "terraform"
113
+
114
+ # Build the provider binary
115
+ pout(f"Building provider in {self.generator.provider_dir}")
116
+ try:
117
+ build_result = run_command(
118
+ ["python", "-m", "build"],
119
+ cwd=self.generator.provider_dir,
120
+ capture_output=True,
121
+ )
122
+ except ProcessError as e:
123
+ logger.error("Provider build failed", command=e.cmd, returncode=e.returncode,
124
+ stdout=e.stdout, stderr=e.stderr)
125
+ raise SchemaError(f"Failed to build provider: {e}")
126
+
127
+ # Find the built provider binary
128
+ provider_binary = self._find_provider_binary()
129
+
130
+ # Create a temporary directory for Terraform operations
131
+ temp_dir = self.generator.provider_dir / ".pyvbuild_temp"
132
+ temp_dir.mkdir(exist_ok=True)
133
+
134
+ try:
135
+ # Create basic Terraform configuration
136
+ tf_config = f'''
137
+ terraform {{
138
+ required_providers {{
139
+ {self.generator.provider_name} = {{
140
+ source = "local/providers/{self.generator.provider_name}"
141
+ }}
142
+ }}
143
+ }}
144
+
145
+ provider "{self.generator.provider_name}" {{}}
146
+ '''
147
+
148
+ tf_file = temp_dir / "main.tf"
149
+ tf_file.write_text(tf_config)
150
+
151
+ # Initialize Terraform
152
+ try:
153
+ run_command(
154
+ [tf_binary, "init"],
155
+ cwd=temp_dir,
156
+ capture_output=True,
157
+ )
158
+ except ProcessError as e:
159
+ logger.error("Terraform init failed", command=e.cmd, returncode=e.returncode,
160
+ stdout=e.stdout, stderr=e.stderr)
161
+ raise SchemaError(f"Failed to initialize Terraform: {e}")
162
+
163
+ # Extract schema
164
+ try:
165
+ schema_result = run_command(
166
+ [tf_binary, "providers", "schema", "-json"],
167
+ cwd=temp_dir,
168
+ capture_output=True,
169
+ )
170
+ except ProcessError as e:
171
+ logger.error("Schema extraction failed", command=e.cmd, returncode=e.returncode,
172
+ stdout=e.stdout, stderr=e.stderr)
173
+ raise SchemaError(f"Failed to extract provider schema: {e}")
174
+
175
+ schema_data = json.loads(schema_result.stdout)
176
+ return schema_data
177
+
178
+ finally:
179
+ # Clean up temporary directory
180
+ shutil.rmtree(temp_dir, ignore_errors=True)
181
+
182
+ def _find_provider_binary(self) -> Path:
183
+ """Find the provider binary after building."""
184
+ # Look for the provider binary in common locations
185
+ binary_paths = [
186
+ self.generator.provider_dir / "terraform-provider-*",
187
+ self.generator.provider_dir / "dist" / "terraform-provider-*",
188
+ self.generator.provider_dir / "bin" / "terraform-provider-*",
189
+ ]
190
+
191
+ import glob
192
+
193
+ for pattern in binary_paths:
194
+ matches = glob.glob(str(pattern))
195
+ if matches:
196
+ return Path(matches[0])
197
+
198
+ raise FileNotFoundError(
199
+ f"Could not find provider binary for {self.generator.provider_name}"
200
+ )
201
+
202
+ def _parse_function_signature(self, func_schema: dict[str, Any]) -> str:
203
+ """Parse function signature from schema."""
204
+ if "signature" not in func_schema:
205
+ return ""
206
+
207
+ signature = func_schema["signature"]
208
+ params = []
209
+
210
+ # Handle parameters
211
+ if "parameters" in signature:
212
+ for param in signature["parameters"]:
213
+ param_name = param.get("name", "arg")
214
+ param_type = param.get("type", "any")
215
+ params.append(f"{param_name}: {param_type}")
216
+
217
+ # Handle variadic parameter
218
+ if "variadic_parameter" in signature:
219
+ variadic = signature["variadic_parameter"]
220
+ variadic_name = variadic.get("name", "args")
221
+ variadic_type = variadic.get("type", "any")
222
+ params.append(f"...{variadic_name}: {variadic_type}")
223
+
224
+ # Handle return type
225
+ return_type = signature.get("return_type", "any")
226
+
227
+ param_str = ", ".join(params)
228
+ return f"function({param_str}) -> {return_type}"
229
+
230
+ def _parse_function_arguments(self, func_schema: dict[str, Any]) -> str:
231
+ """Parse function arguments from schema."""
232
+ if "signature" not in func_schema:
233
+ return ""
234
+
235
+ signature = func_schema["signature"]
236
+ lines = []
237
+
238
+ # Handle parameters
239
+ if "parameters" in signature:
240
+ for param in signature["parameters"]:
241
+ param_name = param.get("name", "arg")
242
+ param_type = param.get("type", "any")
243
+ description = param.get("description", "")
244
+ lines.append(f"- `{param_name}` ({param_type}) - {description}")
245
+
246
+ return "\n".join(lines)
247
+
248
+ def _parse_variadic_argument(self, func_schema: dict[str, Any]) -> str:
249
+ """Parse variadic argument from schema."""
250
+ if (
251
+ "signature" not in func_schema
252
+ or "variadic_parameter" not in func_schema["signature"]
253
+ ):
254
+ return ""
255
+
256
+ variadic = func_schema["signature"]["variadic_parameter"]
257
+ variadic_name = variadic.get("name", "args")
258
+ variadic_type = variadic.get("type", "any")
259
+ description = variadic.get("description", "")
260
+
261
+ return f"- `{variadic_name}` ({variadic_type}) - {description}"
262
+
263
+ def parse_provider_schema(self):
264
+ """Parse extracted provider schema into internal structures."""
265
+ schema = self.generator.provider_schema
266
+ if not schema:
267
+ return
268
+
269
+ # Create provider info
270
+ provider_schema = schema.get("provider_schemas", {}).get(
271
+ f"registry.terraform.io/local/providers/{self.generator.provider_name}", {}
272
+ )
273
+ provider_config_schema = provider_schema.get("provider", {})
274
+
275
+ self.generator.provider_info = ProviderInfo(
276
+ name=self.generator.provider_name,
277
+ description=provider_config_schema.get(
278
+ "description", f"Terraform provider for {self.generator.provider_name}"
279
+ ),
280
+ short_name=self.generator.provider_name,
281
+ rendered_name=self.generator.rendered_provider_name,
282
+ )
283
+
284
+ # Process resources
285
+ resources = provider_schema.get("resource_schemas", {})
286
+ if isinstance(resources, tuple):
287
+ resources = {}
288
+ for resource_name, resource_schema in resources.items():
289
+ if self.generator.ignore_deprecated and resource_schema.get(
290
+ "deprecated", False
291
+ ):
292
+ continue
293
+
294
+ schema_markdown = self._parse_schema_to_markdown(resource_schema)
295
+
296
+ self.generator.resources[resource_name] = ResourceInfo(
297
+ name=resource_name,
298
+ type="Resource",
299
+ description=resource_schema.get("description", ""),
300
+ schema_markdown=schema_markdown,
301
+ schema=resource_schema,
302
+ )
303
+
304
+ # Process data sources
305
+ data_sources = provider_schema.get("data_source_schemas", {})
306
+ for ds_name, ds_schema in data_sources.items():
307
+ if self.generator.ignore_deprecated and ds_schema.get("deprecated", False):
308
+ continue
309
+
310
+ schema_markdown = self._parse_schema_to_markdown(ds_schema)
311
+
312
+ self.generator.data_sources[ds_name] = ResourceInfo(
313
+ name=ds_name,
314
+ type="Data Source",
315
+ description=ds_schema.get("description", ""),
316
+ schema_markdown=schema_markdown,
317
+ schema=ds_schema,
318
+ )
319
+
320
+ # Process functions
321
+ functions = provider_schema.get("functions", {})
322
+ for func_name, func_schema in functions.items():
323
+ signature_markdown = self._parse_function_signature(func_schema)
324
+ arguments_markdown = self._parse_function_arguments(func_schema)
325
+ variadic_markdown = self._parse_variadic_argument(func_schema)
326
+
327
+ self.generator.functions[func_name] = FunctionInfo(
328
+ name=func_name,
329
+ description=func_schema.get("description", ""),
330
+ summary=func_schema.get("summary", ""),
331
+ signature_markdown=signature_markdown,
332
+ arguments_markdown=arguments_markdown,
333
+ has_variadic="variadic_parameter" in func_schema.get("signature", {}),
334
+ variadic_argument_markdown=variadic_markdown,
335
+ )
336
+
337
+ def _parse_schema_to_markdown(self, schema: dict[str, Any]) -> str:
338
+ """Parse a schema object into markdown documentation."""
339
+ if not schema:
340
+ return ""
341
+
342
+ # Extract block information
343
+ block = schema.get("block", {})
344
+ if not block:
345
+ return ""
346
+
347
+ markdown_lines = []
348
+
349
+ # Handle attributes
350
+ attributes = block.get("attributes", {})
351
+ if attributes:
352
+ markdown_lines.append("## Arguments\n")
353
+ for attr_name, attr_spec in attributes.items():
354
+ description = attr_spec.get("description", "")
355
+ attr_type_raw = attr_spec.get("type", {})
356
+ attr_type = self._format_type_string(attr_type_raw)
357
+ required = attr_spec.get("required", False)
358
+ optional = attr_spec.get("optional", False)
359
+ computed = attr_spec.get("computed", False)
360
+
361
+ # Determine characteristics
362
+ characteristics = []
363
+ if required:
364
+ characteristics.append("Required")
365
+ elif optional:
366
+ characteristics.append("Optional")
367
+ elif computed:
368
+ characteristics.append("Computed")
369
+
370
+ # Format like tfplugindocs: (Type, Characteristics)
371
+ if characteristics:
372
+ type_text = f"({attr_type}, {', '.join(characteristics)})"
373
+ else:
374
+ type_text = f"({attr_type})"
375
+
376
+ markdown_lines.append(
377
+ f"- `{attr_name}` {type_text} {description}".strip()
378
+ )
379
+
380
+ markdown_lines.append("")
381
+
382
+ # Handle nested blocks
383
+ nested_blocks = block.get("block_types", {})
384
+ if nested_blocks and isinstance(nested_blocks, dict):
385
+ markdown_lines.append("## Blocks\n")
386
+ for block_name, block_spec in nested_blocks.items():
387
+ description = block_spec.get("description", "")
388
+ nesting_mode = block_spec.get("nesting_mode", "single")
389
+
390
+ markdown_lines.append(f"### {block_name}")
391
+ if description:
392
+ markdown_lines.append(f"\n{description}\n")
393
+
394
+ # Handle block attributes
395
+ block_attrs = block_spec.get("block", {}).get("attributes", {})
396
+ if block_attrs:
397
+ for attr_name, attr_spec in block_attrs.items():
398
+ attr_description = attr_spec.get("description", "")
399
+ attr_type = attr_spec.get("type", "unknown")
400
+ required = attr_spec.get("required", False)
401
+ optional = attr_spec.get("optional", False)
402
+ computed = attr_spec.get("computed", False)
403
+
404
+ if required:
405
+ req_text = " (Required)"
406
+ elif optional:
407
+ req_text = " (Optional)"
408
+ elif computed:
409
+ req_text = " (Computed)"
410
+ else:
411
+ req_text = ""
412
+
413
+ markdown_lines.append(
414
+ f"- `{attr_name}` ({attr_type}){req_text} - {attr_description}"
415
+ )
416
+
417
+ markdown_lines.append("")
418
+
419
+ return "\n".join(markdown_lines)
420
+
421
+ def _format_type_string(self, type_info: Any) -> str:
422
+ """Convert a type object to a human-readable type string."""
423
+ if not type_info:
424
+ return "String" # Default fallback
425
+
426
+ # Handle CTY type objects
427
+ try:
428
+ # Import here to avoid circular imports
429
+ from pyvider.cty import (
430
+ CtyBool,
431
+ CtyDynamic,
432
+ CtyList,
433
+ CtyMap,
434
+ CtyNumber,
435
+ CtyObject,
436
+ CtySet,
437
+ CtyString,
438
+ )
439
+
440
+ if hasattr(type_info, "__class__"):
441
+ type_class = type_info.__class__
442
+ if type_class == CtyString:
443
+ return "String"
444
+ elif type_class == CtyNumber:
445
+ return "Number"
446
+ elif type_class == CtyBool:
447
+ return "Boolean"
448
+ elif type_class == CtyList:
449
+ element_type = self._format_type_string(
450
+ getattr(type_info, "element_type", None)
451
+ )
452
+ return f"List of {element_type}"
453
+ elif type_class == CtySet:
454
+ element_type = self._format_type_string(
455
+ getattr(type_info, "element_type", None)
456
+ )
457
+ return f"Set of {element_type}"
458
+ elif type_class == CtyMap:
459
+ element_type = self._format_type_string(
460
+ getattr(type_info, "element_type", None)
461
+ )
462
+ return f"Map of {element_type}"
463
+ elif type_class == CtyObject:
464
+ return "Object"
465
+ elif type_class == CtyDynamic:
466
+ return "Dynamic"
467
+ except (ImportError, AttributeError):
468
+ pass
469
+
470
+ # Handle string representations
471
+ if isinstance(type_info, str):
472
+ type_str = type_info.lower()
473
+ if "string" in type_str:
474
+ return "String"
475
+ elif "number" in type_str or "int" in type_str or "float" in type_str:
476
+ return "Number"
477
+ elif "bool" in type_str:
478
+ return "Boolean"
479
+ elif "list" in type_str:
480
+ return "List of String"
481
+ elif "set" in type_str:
482
+ return "Set of String"
483
+ elif "map" in type_str:
484
+ return "Map of String"
485
+ elif "object" in type_str:
486
+ return "Object"
487
+
488
+ # Handle dict representations (from schema extraction)
489
+ if isinstance(type_info, dict):
490
+ # Check if it's an empty dict (common case we saw)
491
+ if not type_info:
492
+ return "String" # Default fallback
493
+
494
+ # Try to infer from dict structure
495
+ if "type" in type_info:
496
+ return self._format_type_string(type_info["type"])
497
+
498
+ # Final fallback
499
+ return "String"
500
+
501
+
502
+ # 🍲🥄📊🪄
@@ -0,0 +1,117 @@
1
+ #
2
+ # plating/template_filters.py
3
+ #
4
+ """Custom Jinja2 filters for documentation generation."""
5
+
6
+ from typing import Any
7
+
8
+
9
+ def schema_to_markdown(schema: Any, prefix: str = "") -> str:
10
+ """Convert a PvsSchema or a dictionary to markdown documentation."""
11
+ if not schema:
12
+ return ""
13
+
14
+ lines = []
15
+
16
+ block = schema
17
+ if hasattr(schema, "block"):
18
+ block = schema.block
19
+
20
+ # Handle nested blocks
21
+ if hasattr(block, "attributes") and hasattr(block, "block_types"):
22
+ attributes = block.attributes
23
+ nested_blocks = block.block_types
24
+
25
+ # Process attributes
26
+ for attr_name, attr_schema in attributes.items():
27
+ attr_type = (
28
+ attr_schema.type.__name__ if hasattr(attr_schema, "type") else "string"
29
+ )
30
+ description = (
31
+ attr_schema.description if hasattr(attr_schema, "description") else ""
32
+ )
33
+ required = (
34
+ attr_schema.required if hasattr(attr_schema, "required") else False
35
+ )
36
+ optional = (
37
+ attr_schema.optional if hasattr(attr_schema, "optional") else False
38
+ )
39
+ computed = (
40
+ attr_schema.computed if hasattr(attr_schema, "computed") else False
41
+ )
42
+
43
+ status = []
44
+ if required:
45
+ status.append("Required")
46
+ elif optional:
47
+ status.append("Optional")
48
+ if computed:
49
+ status.append("Computed")
50
+
51
+ status_str = ", ".join(status) if status else "Optional"
52
+
53
+ lines.append(
54
+ f"- `{prefix}{attr_name}` ({attr_type}) - {description} ({status_str})"
55
+ )
56
+
57
+ # Process nested blocks
58
+ for block_name, block_schema in nested_blocks.items():
59
+ lines.append(
60
+ f"- `{prefix}{block_name}` - {block_schema.description if hasattr(block_schema, 'description') else ''}"
61
+ )
62
+ nested_markdown = schema_to_markdown(block_schema, f"{prefix}{block_name}.")
63
+ lines.append(nested_markdown)
64
+
65
+ return "\n".join(lines)
66
+
67
+
68
+ def attrs_schema_to_markdown(schema: dict[str, Any], prefix: str = "") -> str:
69
+ """Convert a dictionary from attrs.asdict to markdown documentation."""
70
+ if not schema:
71
+ return ""
72
+
73
+ lines = []
74
+
75
+ # Handle nested blocks
76
+ if "block" in schema and "attributes" in schema["block"]:
77
+ attributes = schema["block"]["attributes"]
78
+ nested_blocks = schema["block"].get("block_types", [])
79
+
80
+ # Process attributes
81
+ for attr_name, attr_schema in attributes.items():
82
+ attr_type = attr_schema.get("type", {}).get("_name", "string")
83
+ description = attr_schema.get("description", "")
84
+ required = attr_schema.get("required", False)
85
+ optional = attr_schema.get("optional", False)
86
+ computed = attr_schema.get("computed", False)
87
+
88
+ status = []
89
+ if required:
90
+ status.append("Required")
91
+ elif optional:
92
+ status.append("Optional")
93
+ if computed:
94
+ status.append("Computed")
95
+
96
+ status_str = ", ".join(status) if status else "Optional"
97
+
98
+ lines.append(
99
+ f"- `{prefix}{attr_name}` ({attr_type}) - {description} ({status_str})"
100
+ )
101
+
102
+ # Process nested blocks
103
+ for block in nested_blocks:
104
+ if isinstance(block, dict):
105
+ block_name = block.get("type_name", "")
106
+ lines.append(
107
+ f"- `{prefix}{block_name}` - {block.get('description', '')}"
108
+ )
109
+ nested_markdown = attrs_schema_to_markdown(
110
+ block.get("block", {}), f"{prefix}{block_name}."
111
+ )
112
+ lines.append(nested_markdown)
113
+
114
+ return "\n".join(lines)
115
+
116
+
117
+ # 🍲🥄📄🪄