devconfig-gen-singbox 2.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.
@@ -0,0 +1,67 @@
1
+ """Provider-based developer configuration generation.
2
+
3
+ The package exposes the provider pipeline and the stable CLI/Python API only.
4
+ Importing it has no side effects.
5
+ """
6
+
7
+ from .engine import (
8
+ build_request,
9
+ describe_provider,
10
+ diagnose_request,
11
+ generate,
12
+ generate_from_file,
13
+ generate_pipeline,
14
+ validate_request,
15
+ )
16
+ from .formats import (
17
+ FormatError,
18
+ coerce_scalar,
19
+ deep_merge,
20
+ dump_data,
21
+ dump_file,
22
+ dumps,
23
+ load_data,
24
+ load_file,
25
+ loads,
26
+ )
27
+ from .models import (
28
+ ConfigProvider,
29
+ Diagnostic,
30
+ GeneratedArtifact,
31
+ GenerationRequest,
32
+ GenerationResult,
33
+ ProviderField,
34
+ ProviderStep,
35
+ )
36
+ from .registry import ProviderRegistry, default_registry
37
+ from .validation import ValidationError
38
+
39
+ __all__ = [
40
+ "ConfigProvider",
41
+ "Diagnostic",
42
+ "FormatError",
43
+ "GeneratedArtifact",
44
+ "GenerationRequest",
45
+ "GenerationResult",
46
+ "ProviderField",
47
+ "ProviderRegistry",
48
+ "ProviderStep",
49
+ "ValidationError",
50
+ "build_request",
51
+ "coerce_scalar",
52
+ "deep_merge",
53
+ "default_registry",
54
+ "describe_provider",
55
+ "diagnose_request",
56
+ "dump_data",
57
+ "dump_file",
58
+ "dumps",
59
+ "generate",
60
+ "generate_from_file",
61
+ "generate_pipeline",
62
+ "load_data",
63
+ "load_file",
64
+ "loads",
65
+ "validate_request",
66
+ ]
67
+ __version__ = "2.0.0"
devconfig_gen/cli.py ADDED
@@ -0,0 +1,163 @@
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 __version__, 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=f"%(prog)s {__version__}")
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
+ return parser
78
+
79
+
80
+ def _list_providers(args) -> int:
81
+ for name in default_registry.names():
82
+ print(name)
83
+ return 0
84
+
85
+
86
+ def _parse_overrides(pairs) -> dict:
87
+ overrides = {}
88
+ for item in pairs or ():
89
+ if "=" not in item:
90
+ raise ValueError(f"--set expects KEY=VALUE, got {item!r}")
91
+ key, value = item.split("=", 1)
92
+ key = key.strip()
93
+ if not key:
94
+ raise ValueError(f"--set expects a non-empty key, got {item!r}")
95
+ overrides[key] = formats.coerce_scalar(value)
96
+ return overrides
97
+
98
+
99
+ def _generate(args) -> int:
100
+ options = {"name": args.name} if args.name else None
101
+ try:
102
+ overrides = _parse_overrides(args.overrides)
103
+ result = generate_pipeline(
104
+ args.provider,
105
+ input_path=[str(path) for path in args.input],
106
+ output_dir=str(args.output_dir),
107
+ output_format=args.format,
108
+ options=options,
109
+ overrides=overrides,
110
+ )
111
+ except (OSError, formats.FormatError, ValueError) as exc:
112
+ print(f"error: {exc}", file=sys.stderr)
113
+ return 2
114
+ for artifact in result.artifacts:
115
+ print(f"generated {args.output_dir / artifact.name}")
116
+ return 0
117
+
118
+
119
+ def _validate(args) -> int:
120
+ try:
121
+ overrides = _parse_overrides(args.overrides)
122
+ diagnostics = diagnose_request(
123
+ args.provider,
124
+ input_path=[str(path) for path in args.input],
125
+ input_format=args.format,
126
+ overrides=overrides,
127
+ )
128
+ except (OSError, formats.FormatError, ValueError) as exc:
129
+ print(f"error: {exc}", file=sys.stderr)
130
+ return 2
131
+
132
+ if args.json:
133
+ print(json.dumps([item.as_dict() for item in diagnostics], indent=2))
134
+
135
+ errors = [item for item in diagnostics if item.severity == "error"]
136
+ if errors:
137
+ if not args.json:
138
+ for item in errors:
139
+ print(f"invalid: {item.message}", file=sys.stderr)
140
+ return 1
141
+ if not args.json:
142
+ sources = ", ".join(str(path) for path in args.input)
143
+ print(f"{sources}: valid")
144
+ return 0
145
+
146
+
147
+ def _schema(args) -> int:
148
+ try:
149
+ steps = describe_provider(args.provider)
150
+ except (OSError, formats.FormatError, ValueError) as exc:
151
+ print(f"error: {exc}", file=sys.stderr)
152
+ return 2
153
+ print(json.dumps([step.as_dict() for step in steps], indent=2))
154
+ return 0
155
+
156
+
157
+ def main(argv=None) -> int:
158
+ args = build_parser().parse_args(argv)
159
+ return args.handler(args)
160
+
161
+
162
+ if __name__ == "__main__":
163
+ raise SystemExit(main())
@@ -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)