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/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
|
+
# 🍲🥄📄🪄
|