devconfig-gen 1.0.0__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.
- devconfig_gen/__init__.py +90 -0
- devconfig_gen/cli.py +213 -0
- devconfig_gen/engine.py +217 -0
- devconfig_gen/formats.py +708 -0
- devconfig_gen/interactive.py +377 -0
- devconfig_gen/models.py +135 -0
- devconfig_gen/providers/__init__.py +7 -0
- devconfig_gen/providers/custom.py +92 -0
- devconfig_gen/providers/env_provider.py +184 -0
- devconfig_gen/providers/json_provider.py +84 -0
- devconfig_gen/py.typed +0 -0
- devconfig_gen/registry.py +38 -0
- devconfig_gen/validation.py +148 -0
- devconfig_gen/web_ui.py +2143 -0
- devconfig_gen-1.0.0.dist-info/METADATA +456 -0
- devconfig_gen-1.0.0.dist-info/RECORD +20 -0
- devconfig_gen-1.0.0.dist-info/WHEEL +5 -0
- devconfig_gen-1.0.0.dist-info/entry_points.txt +2 -0
- devconfig_gen-1.0.0.dist-info/licenses/LICENSE +201 -0
- devconfig_gen-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Provider-based developer configuration generation.
|
|
2
|
+
|
|
3
|
+
The core package exposes the provider pipeline only. The interactive terminal
|
|
4
|
+
wizard and the local web studio are imported lazily so that ``import
|
|
5
|
+
devconfig_gen`` stays lightweight and free of any server or browser imports.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .engine import (
|
|
9
|
+
build_request,
|
|
10
|
+
describe_provider,
|
|
11
|
+
diagnose_request,
|
|
12
|
+
generate,
|
|
13
|
+
generate_from_file,
|
|
14
|
+
generate_pipeline,
|
|
15
|
+
validate_request,
|
|
16
|
+
)
|
|
17
|
+
from .formats import (
|
|
18
|
+
FormatError,
|
|
19
|
+
coerce_scalar,
|
|
20
|
+
deep_merge,
|
|
21
|
+
dump_data,
|
|
22
|
+
dump_file,
|
|
23
|
+
dumps,
|
|
24
|
+
load_data,
|
|
25
|
+
load_file,
|
|
26
|
+
loads,
|
|
27
|
+
)
|
|
28
|
+
from .models import (
|
|
29
|
+
ConfigProvider,
|
|
30
|
+
Diagnostic,
|
|
31
|
+
GeneratedArtifact,
|
|
32
|
+
GenerationRequest,
|
|
33
|
+
GenerationResult,
|
|
34
|
+
ProviderField,
|
|
35
|
+
ProviderStep,
|
|
36
|
+
)
|
|
37
|
+
from .registry import ProviderRegistry, default_registry
|
|
38
|
+
from .validation import ValidationError
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"ConfigProvider",
|
|
42
|
+
"Diagnostic",
|
|
43
|
+
"FormatError",
|
|
44
|
+
"GeneratedArtifact",
|
|
45
|
+
"GenerationRequest",
|
|
46
|
+
"GenerationResult",
|
|
47
|
+
"ProviderField",
|
|
48
|
+
"ProviderRegistry",
|
|
49
|
+
"ProviderStep",
|
|
50
|
+
"ValidationError",
|
|
51
|
+
"build_request",
|
|
52
|
+
"coerce_scalar",
|
|
53
|
+
"deep_merge",
|
|
54
|
+
"default_registry",
|
|
55
|
+
"describe_provider",
|
|
56
|
+
"diagnose_request",
|
|
57
|
+
"dump_data",
|
|
58
|
+
"dump_file",
|
|
59
|
+
"dumps",
|
|
60
|
+
"generate",
|
|
61
|
+
"generate_from_file",
|
|
62
|
+
"generate_pipeline",
|
|
63
|
+
"load_data",
|
|
64
|
+
"load_file",
|
|
65
|
+
"loads",
|
|
66
|
+
"run_interactive_wizard",
|
|
67
|
+
"run_web_ui",
|
|
68
|
+
"validate_request",
|
|
69
|
+
]
|
|
70
|
+
__version__ = "0.3.0"
|
|
71
|
+
|
|
72
|
+
_LAZY_EXPORTS = {
|
|
73
|
+
"run_interactive_wizard": ("interactive", "run_interactive_wizard"),
|
|
74
|
+
"run_web_ui": ("web_ui", "run_web_ui"),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def __getattr__(name):
|
|
79
|
+
"""Import the optional UI entry points on first use (PEP 562)."""
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
module_name, attribute = _LAZY_EXPORTS[name]
|
|
83
|
+
except KeyError:
|
|
84
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
85
|
+
import importlib
|
|
86
|
+
|
|
87
|
+
module = importlib.import_module(f".{module_name}", __name__)
|
|
88
|
+
value = getattr(module, attribute)
|
|
89
|
+
globals()[name] = value
|
|
90
|
+
return value
|
devconfig_gen/cli.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Command-line interface for the generic generation engine."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import formats
|
|
11
|
+
from .engine import describe_provider, diagnose_request, generate_pipeline
|
|
12
|
+
from .registry import default_registry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="devconfig-gen",
|
|
18
|
+
description="Generate and validate structured JSON/YAML configuration.",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument("--version", action="version", version="%(prog)s 1.0.0")
|
|
21
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
|
|
23
|
+
providers = sub.add_parser("providers", help="List available providers")
|
|
24
|
+
providers.set_defaults(handler=_list_providers)
|
|
25
|
+
|
|
26
|
+
generate_cmd = sub.add_parser("generate", help="Generate configuration from structured input")
|
|
27
|
+
generate_cmd.add_argument("--provider", default="json", help="Provider name (default: json)")
|
|
28
|
+
generate_cmd.add_argument(
|
|
29
|
+
"--input",
|
|
30
|
+
required=True,
|
|
31
|
+
action="append",
|
|
32
|
+
type=Path,
|
|
33
|
+
help="JSON or YAML input file (repeatable; merged left to right)",
|
|
34
|
+
)
|
|
35
|
+
generate_cmd.add_argument("--output-dir", required=True, type=Path, help="Directory for artifacts")
|
|
36
|
+
generate_cmd.add_argument("--format", choices=("json", "yaml"), help="Output format")
|
|
37
|
+
generate_cmd.add_argument("--name", help="Output file name (default: provider-specific)")
|
|
38
|
+
generate_cmd.add_argument(
|
|
39
|
+
"--set",
|
|
40
|
+
action="append",
|
|
41
|
+
default=[],
|
|
42
|
+
dest="overrides",
|
|
43
|
+
metavar="KEY=VALUE",
|
|
44
|
+
help="Override a value by dotted path, e.g. app.port=9090 (repeatable)",
|
|
45
|
+
)
|
|
46
|
+
generate_cmd.set_defaults(handler=_generate)
|
|
47
|
+
|
|
48
|
+
validate_cmd = sub.add_parser("validate", help="Validate input without generating output")
|
|
49
|
+
validate_cmd.add_argument("--provider", default="json", help="Provider name (default: json)")
|
|
50
|
+
validate_cmd.add_argument(
|
|
51
|
+
"--input",
|
|
52
|
+
required=True,
|
|
53
|
+
action="append",
|
|
54
|
+
type=Path,
|
|
55
|
+
help="JSON or YAML input file (repeatable; merged left to right)",
|
|
56
|
+
)
|
|
57
|
+
validate_cmd.add_argument("--format", choices=("json", "yaml"), help="Input format override")
|
|
58
|
+
validate_cmd.add_argument(
|
|
59
|
+
"--set",
|
|
60
|
+
action="append",
|
|
61
|
+
default=[],
|
|
62
|
+
dest="overrides",
|
|
63
|
+
metavar="KEY=VALUE",
|
|
64
|
+
help="Override a value by dotted path before validating (repeatable)",
|
|
65
|
+
)
|
|
66
|
+
validate_cmd.add_argument(
|
|
67
|
+
"--json", action="store_true", help="Print structured diagnostics as JSON"
|
|
68
|
+
)
|
|
69
|
+
validate_cmd.set_defaults(handler=_validate)
|
|
70
|
+
|
|
71
|
+
schema_cmd = sub.add_parser(
|
|
72
|
+
"schema", help="Print a provider's declarative field schema as JSON"
|
|
73
|
+
)
|
|
74
|
+
schema_cmd.add_argument("--provider", default="json", help="Provider name (default: json)")
|
|
75
|
+
schema_cmd.set_defaults(handler=_schema)
|
|
76
|
+
|
|
77
|
+
init_cmd = sub.add_parser(
|
|
78
|
+
"init",
|
|
79
|
+
help="Run interactive terminal wizard to create configuration",
|
|
80
|
+
description="Run interactive terminal wizard to create configuration.",
|
|
81
|
+
)
|
|
82
|
+
init_cmd.add_argument("--provider", default="custom", help="Provider name (default: custom)")
|
|
83
|
+
init_cmd.add_argument("--input", type=Path, help="Optional existing config file to pre-fill wizard")
|
|
84
|
+
init_cmd.add_argument("--output-dir", default=".", type=Path, help="Directory for generated artifact")
|
|
85
|
+
init_cmd.add_argument("--format", choices=("json", "yaml"), help="Output format override")
|
|
86
|
+
init_cmd.set_defaults(handler=_init)
|
|
87
|
+
|
|
88
|
+
ui_cmd = sub.add_parser(
|
|
89
|
+
"ui",
|
|
90
|
+
help="Launch local configuration studio WebUI",
|
|
91
|
+
description="Launch local configuration studio WebUI.",
|
|
92
|
+
)
|
|
93
|
+
ui_cmd.add_argument("--host", default="127.0.0.1", help="Host address (default: 127.0.0.1)")
|
|
94
|
+
ui_cmd.add_argument("--port", default=8848, type=int, help="Port to bind (default: 8848)")
|
|
95
|
+
ui_cmd.add_argument("--no-browser", action="store_true", help="Do not automatically open browser")
|
|
96
|
+
ui_cmd.add_argument(
|
|
97
|
+
"--workspace",
|
|
98
|
+
default=None,
|
|
99
|
+
help="Directory the studio may export into (default: current directory)",
|
|
100
|
+
)
|
|
101
|
+
ui_cmd.set_defaults(handler=_ui)
|
|
102
|
+
|
|
103
|
+
return parser
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _list_providers(args) -> int:
|
|
107
|
+
for name in default_registry.names():
|
|
108
|
+
print(name)
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _parse_overrides(pairs) -> dict:
|
|
113
|
+
overrides = {}
|
|
114
|
+
for item in pairs or ():
|
|
115
|
+
if "=" not in item:
|
|
116
|
+
raise ValueError(f"--set expects KEY=VALUE, got {item!r}")
|
|
117
|
+
key, value = item.split("=", 1)
|
|
118
|
+
key = key.strip()
|
|
119
|
+
if not key:
|
|
120
|
+
raise ValueError(f"--set expects a non-empty key, got {item!r}")
|
|
121
|
+
overrides[key] = formats.coerce_scalar(value)
|
|
122
|
+
return overrides
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _generate(args) -> int:
|
|
126
|
+
options = {"name": args.name} if args.name else None
|
|
127
|
+
try:
|
|
128
|
+
overrides = _parse_overrides(args.overrides)
|
|
129
|
+
result = generate_pipeline(
|
|
130
|
+
args.provider,
|
|
131
|
+
input_path=[str(path) for path in args.input],
|
|
132
|
+
output_dir=str(args.output_dir),
|
|
133
|
+
output_format=args.format,
|
|
134
|
+
options=options,
|
|
135
|
+
overrides=overrides,
|
|
136
|
+
)
|
|
137
|
+
except (OSError, formats.FormatError, ValueError) as exc:
|
|
138
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
139
|
+
return 2
|
|
140
|
+
for artifact in result.artifacts:
|
|
141
|
+
print(f"generated {args.output_dir / artifact.name}")
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _validate(args) -> int:
|
|
146
|
+
try:
|
|
147
|
+
overrides = _parse_overrides(args.overrides)
|
|
148
|
+
diagnostics = diagnose_request(
|
|
149
|
+
args.provider,
|
|
150
|
+
input_path=[str(path) for path in args.input],
|
|
151
|
+
input_format=args.format,
|
|
152
|
+
overrides=overrides,
|
|
153
|
+
)
|
|
154
|
+
except (OSError, formats.FormatError, ValueError) as exc:
|
|
155
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
156
|
+
return 2
|
|
157
|
+
|
|
158
|
+
if args.json:
|
|
159
|
+
print(json.dumps([item.as_dict() for item in diagnostics], indent=2))
|
|
160
|
+
|
|
161
|
+
errors = [item for item in diagnostics if item.severity == "error"]
|
|
162
|
+
if errors:
|
|
163
|
+
if not args.json:
|
|
164
|
+
for item in errors:
|
|
165
|
+
print(f"invalid: {item.message}", file=sys.stderr)
|
|
166
|
+
return 1
|
|
167
|
+
if not args.json:
|
|
168
|
+
sources = ", ".join(str(path) for path in args.input)
|
|
169
|
+
print(f"{sources}: valid")
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _schema(args) -> int:
|
|
174
|
+
try:
|
|
175
|
+
steps = describe_provider(args.provider)
|
|
176
|
+
except (OSError, formats.FormatError, ValueError) as exc:
|
|
177
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
178
|
+
return 2
|
|
179
|
+
print(json.dumps([step.as_dict() for step in steps], indent=2))
|
|
180
|
+
return 0
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _init(args) -> int:
|
|
184
|
+
from .interactive import run_interactive_wizard
|
|
185
|
+
|
|
186
|
+
input_path = str(args.input) if args.input else None
|
|
187
|
+
return run_interactive_wizard(
|
|
188
|
+
args.provider,
|
|
189
|
+
input_path=input_path,
|
|
190
|
+
output_dir=str(args.output_dir),
|
|
191
|
+
output_format=args.format,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _ui(args) -> int:
|
|
196
|
+
from .web_ui import run_web_ui
|
|
197
|
+
|
|
198
|
+
run_web_ui(
|
|
199
|
+
host=args.host,
|
|
200
|
+
port=args.port,
|
|
201
|
+
open_browser=not args.no_browser,
|
|
202
|
+
workspace_root=args.workspace,
|
|
203
|
+
)
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main(argv=None) -> int:
|
|
208
|
+
args = build_parser().parse_args(argv)
|
|
209
|
+
return args.handler(args)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
raise SystemExit(main())
|
devconfig_gen/engine.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Provider-independent generation orchestration.
|
|
2
|
+
|
|
3
|
+
Every entry point in this module funnels into :func:`generate`, so the CLI and
|
|
4
|
+
the Python API share one validation, generation, and persistence path.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Mapping, Optional, Sequence, Union
|
|
11
|
+
|
|
12
|
+
from . import formats
|
|
13
|
+
from .models import (
|
|
14
|
+
Diagnostic,
|
|
15
|
+
GeneratedArtifact,
|
|
16
|
+
GenerationRequest,
|
|
17
|
+
GenerationResult,
|
|
18
|
+
ProviderStep,
|
|
19
|
+
)
|
|
20
|
+
from .registry import ProviderRegistry, default_registry
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate(
|
|
24
|
+
provider: str,
|
|
25
|
+
request: GenerationRequest,
|
|
26
|
+
registry: Optional[ProviderRegistry] = None,
|
|
27
|
+
output_dir: Optional[str] = None,
|
|
28
|
+
) -> GenerationResult:
|
|
29
|
+
"""Validate, generate, and optionally persist provider artifacts."""
|
|
30
|
+
|
|
31
|
+
selected = (registry or default_registry).get(provider)
|
|
32
|
+
errors = tuple(selected.validate(request))
|
|
33
|
+
if errors:
|
|
34
|
+
raise ValueError("; ".join(errors))
|
|
35
|
+
result = selected.generate(request)
|
|
36
|
+
if output_dir is not None:
|
|
37
|
+
_persist(result, output_dir)
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _set_nested(target: dict, path: str, value: Any) -> None:
|
|
42
|
+
parts = path.split(".")
|
|
43
|
+
current = target
|
|
44
|
+
for part in parts[:-1]:
|
|
45
|
+
if part not in current or not isinstance(current[part], dict):
|
|
46
|
+
current[part] = {}
|
|
47
|
+
current = current[part]
|
|
48
|
+
current[parts[-1]] = value
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_request(
|
|
52
|
+
*,
|
|
53
|
+
context: Any = None,
|
|
54
|
+
input_path: Optional[Union[str, Path, Sequence[Any]]] = None,
|
|
55
|
+
options: Optional[Mapping[str, Any]] = None,
|
|
56
|
+
input_format: Optional[str] = None,
|
|
57
|
+
overrides: Optional[Mapping[str, Any]] = None,
|
|
58
|
+
) -> GenerationRequest:
|
|
59
|
+
"""Build a request from an in-memory context and/or multiple input files with overrides."""
|
|
60
|
+
|
|
61
|
+
merged_context = dict(context) if isinstance(context, Mapping) else (context or {})
|
|
62
|
+
if input_path is not None:
|
|
63
|
+
if isinstance(input_path, (str, Path)):
|
|
64
|
+
paths = [input_path]
|
|
65
|
+
elif isinstance(input_path, Sequence):
|
|
66
|
+
paths = list(input_path)
|
|
67
|
+
else:
|
|
68
|
+
paths = [input_path]
|
|
69
|
+
|
|
70
|
+
for p in paths:
|
|
71
|
+
loaded = formats.load_file(p, input_format)
|
|
72
|
+
if isinstance(merged_context, Mapping) and isinstance(loaded, Mapping):
|
|
73
|
+
merged_context = formats.deep_merge(merged_context, loaded)
|
|
74
|
+
else:
|
|
75
|
+
merged_context = loaded
|
|
76
|
+
|
|
77
|
+
if overrides:
|
|
78
|
+
if not isinstance(merged_context, dict):
|
|
79
|
+
merged_context = {}
|
|
80
|
+
for key, val in overrides.items():
|
|
81
|
+
_set_nested(merged_context, key, val)
|
|
82
|
+
|
|
83
|
+
return GenerationRequest(context=merged_context, options=dict(options or {}))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def generate_pipeline(
|
|
87
|
+
provider: str,
|
|
88
|
+
*,
|
|
89
|
+
context: Any = None,
|
|
90
|
+
input_path: Optional[Union[str, Path, Sequence[Any]]] = None,
|
|
91
|
+
options: Optional[Mapping[str, Any]] = None,
|
|
92
|
+
output_dir: Optional[str] = None,
|
|
93
|
+
output_format: Optional[str] = None,
|
|
94
|
+
input_format: Optional[str] = None,
|
|
95
|
+
registry: Optional[ProviderRegistry] = None,
|
|
96
|
+
overrides: Optional[Mapping[str, Any]] = None,
|
|
97
|
+
) -> GenerationResult:
|
|
98
|
+
"""Run the full pipeline from multi-source inputs to optional output."""
|
|
99
|
+
|
|
100
|
+
merged = dict(options or {})
|
|
101
|
+
if output_format is not None:
|
|
102
|
+
merged["format"] = output_format
|
|
103
|
+
request = build_request(
|
|
104
|
+
context=context,
|
|
105
|
+
input_path=input_path,
|
|
106
|
+
options=merged,
|
|
107
|
+
input_format=input_format,
|
|
108
|
+
overrides=overrides,
|
|
109
|
+
)
|
|
110
|
+
return generate(provider, request, registry=registry, output_dir=output_dir)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def generate_from_file(
|
|
114
|
+
provider: str,
|
|
115
|
+
input_path: str,
|
|
116
|
+
*,
|
|
117
|
+
output_dir: Optional[str] = None,
|
|
118
|
+
output_format: Optional[str] = None,
|
|
119
|
+
options: Optional[Mapping[str, Any]] = None,
|
|
120
|
+
registry: Optional[ProviderRegistry] = None,
|
|
121
|
+
) -> GenerationResult:
|
|
122
|
+
"""Convenience wrapper around :func:`generate_pipeline` for file input."""
|
|
123
|
+
|
|
124
|
+
return generate_pipeline(
|
|
125
|
+
provider,
|
|
126
|
+
input_path=input_path,
|
|
127
|
+
output_dir=output_dir,
|
|
128
|
+
output_format=output_format,
|
|
129
|
+
options=options,
|
|
130
|
+
registry=registry,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def validate_request(
|
|
135
|
+
provider: str,
|
|
136
|
+
*,
|
|
137
|
+
context: Any = None,
|
|
138
|
+
input_path: Optional[Union[str, Path, Sequence[Any]]] = None,
|
|
139
|
+
options: Optional[Mapping[str, Any]] = None,
|
|
140
|
+
input_format: Optional[str] = None,
|
|
141
|
+
overrides: Optional[Mapping[str, Any]] = None,
|
|
142
|
+
registry: Optional[ProviderRegistry] = None,
|
|
143
|
+
) -> Sequence[str]:
|
|
144
|
+
"""Return provider validation errors without generating or writing output."""
|
|
145
|
+
|
|
146
|
+
request = build_request(
|
|
147
|
+
context=context,
|
|
148
|
+
input_path=input_path,
|
|
149
|
+
options=options,
|
|
150
|
+
input_format=input_format,
|
|
151
|
+
overrides=overrides,
|
|
152
|
+
)
|
|
153
|
+
selected = (registry or default_registry).get(provider)
|
|
154
|
+
return tuple(selected.validate(request))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def diagnose_request(
|
|
158
|
+
provider: str,
|
|
159
|
+
*,
|
|
160
|
+
context: Any = None,
|
|
161
|
+
input_path: Optional[Union[str, Path, Sequence[Any]]] = None,
|
|
162
|
+
options: Optional[Mapping[str, Any]] = None,
|
|
163
|
+
input_format: Optional[str] = None,
|
|
164
|
+
overrides: Optional[Mapping[str, Any]] = None,
|
|
165
|
+
registry: Optional[ProviderRegistry] = None,
|
|
166
|
+
) -> Sequence[Diagnostic]:
|
|
167
|
+
"""Return structured diagnostics without generating or writing output."""
|
|
168
|
+
|
|
169
|
+
request = build_request(
|
|
170
|
+
context=context,
|
|
171
|
+
input_path=input_path,
|
|
172
|
+
options=options,
|
|
173
|
+
input_format=input_format,
|
|
174
|
+
overrides=overrides,
|
|
175
|
+
)
|
|
176
|
+
selected = (registry or default_registry).get(provider)
|
|
177
|
+
diagnose = getattr(selected, "diagnose", None)
|
|
178
|
+
if callable(diagnose):
|
|
179
|
+
return tuple(diagnose(request))
|
|
180
|
+
return tuple(Diagnostic(field="", message=message) for message in selected.validate(request))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def describe_provider(
|
|
184
|
+
provider: str,
|
|
185
|
+
registry: Optional[ProviderRegistry] = None,
|
|
186
|
+
) -> Sequence[ProviderStep]:
|
|
187
|
+
"""Return a provider's declarative step/field metadata."""
|
|
188
|
+
|
|
189
|
+
selected = (registry or default_registry).get(provider)
|
|
190
|
+
describe = getattr(selected, "describe_schema", None)
|
|
191
|
+
if callable(describe):
|
|
192
|
+
return tuple(describe())
|
|
193
|
+
return tuple(getattr(selected, "steps", ()))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _persist(result: GenerationResult, output_dir: str) -> None:
|
|
197
|
+
destination = Path(output_dir)
|
|
198
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
for artifact in result.artifacts:
|
|
200
|
+
relative_name = Path(artifact.name)
|
|
201
|
+
if relative_name.is_absolute() or ".." in relative_name.parts:
|
|
202
|
+
raise ValueError(f"artifact name escapes output directory: {artifact.name!r}")
|
|
203
|
+
path = destination / relative_name
|
|
204
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
path.write_text(_serialize_artifact(artifact), encoding="utf-8")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _serialize_artifact(artifact: GeneratedArtifact) -> str:
|
|
209
|
+
if isinstance(artifact.content, str):
|
|
210
|
+
return artifact.content
|
|
211
|
+
try:
|
|
212
|
+
fmt = formats.format_from_media_type(artifact.media_type)
|
|
213
|
+
except formats.FormatError as exc:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"cannot persist media type {artifact.media_type!r} for {artifact.name!r}"
|
|
216
|
+
) from exc
|
|
217
|
+
return formats.dumps(artifact.content, fmt)
|