springdocker 1.0.1__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.
springdocker/config.py ADDED
@@ -0,0 +1,365 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ try:
8
+ import tomllib # Python 3.11+
9
+ except ModuleNotFoundError: # pragma: no cover - exercised on Python < 3.11
10
+ import tomli as tomllib
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class DoctorConfig:
15
+ build_tool: str | None
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class DockerfileGenerateConfig:
20
+ build_tool: str | None
21
+ output: str
22
+ java_version: int
23
+ must_have_modules_file: str | None
24
+ wizard_args: list[str]
25
+ use_legacy_scripts: bool
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class BenchmarkGenerateConfig:
30
+ build_tool: str | None
31
+ java_version: int
32
+ use_legacy_scripts: bool
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class BenchmarkRunConfig:
37
+ build_tool: str | None
38
+ profile: str
39
+ runner_args: list[str]
40
+ cpuset_cpus: str | None
41
+ memory_limit: str | None
42
+ warmup_runs: int
43
+ max_workers: int
44
+ normalized_runtime: bool
45
+ use_legacy_scripts: bool
46
+
47
+
48
+ def render_default_config(build_tool: str, profile: str = "quick") -> str:
49
+ """Render starter .springdocker.toml content."""
50
+ if profile not in {"quick", "full"}:
51
+ raise ValueError("benchmark profile must be 'quick' or 'full'")
52
+ return (
53
+ "# springdocker project configuration\n"
54
+ "# Precedence: CLI flags > this file > internal defaults\n\n"
55
+ "[project]\n"
56
+ f'build_tool = "{build_tool}"\n\n'
57
+ "[doctor]\n"
58
+ f'build_tool = "{build_tool}"\n\n'
59
+ "[dockerfile]\n"
60
+ 'output = "Dockerfile.generated"\n'
61
+ "java_version = 25\n"
62
+ '# must_have_modules_file = "must-have.txt"\n'
63
+ "legacy_scripts = false\n"
64
+ "wizard_args = []\n\n"
65
+ "[benchmark.generate]\n"
66
+ "java_version = 25\n"
67
+ "legacy_scripts = false\n\n"
68
+ "[benchmark.run]\n"
69
+ f'profile = "{profile}"\n'
70
+ "runner_args = [\"--skip-native\"]\n"
71
+ "# cpuset_cpus = \"0-1\"\n"
72
+ "# memory_limit = \"2g\"\n"
73
+ "# warmup_runs = 1\n"
74
+ "# max_workers = 1\n"
75
+ "# normalized_runtime = false\n"
76
+ "legacy_scripts = false\n"
77
+ )
78
+
79
+
80
+ def write_default_config(path: Path, build_tool: str, profile: str = "quick", force: bool = False) -> None:
81
+ """Write starter config file; fail if present unless force=True."""
82
+ if path.exists() and not force:
83
+ raise FileExistsError(f"Config already exists: {path}")
84
+ path.write_text(render_default_config(build_tool=build_tool, profile=profile), encoding="utf-8")
85
+
86
+
87
+ def _expect_table(root: dict[str, Any], key: str) -> dict[str, Any]:
88
+ value = root.get(key, {})
89
+ if not isinstance(value, dict):
90
+ raise ValueError(f"Config section '{key}' must be a TOML table")
91
+ return value
92
+
93
+
94
+ def _expect_optional_str(value: Any, key: str) -> str | None:
95
+ if value is None:
96
+ return None
97
+ if not isinstance(value, str):
98
+ raise ValueError(f"Config key '{key}' must be a string")
99
+ return value
100
+
101
+
102
+ def _expect_optional_int(value: Any, key: str) -> int | None:
103
+ if value is None:
104
+ return None
105
+ if not isinstance(value, int):
106
+ raise ValueError(f"Config key '{key}' must be an integer")
107
+ return value
108
+
109
+
110
+ def _expect_optional_bool(value: Any, key: str) -> bool | None:
111
+ if value is None:
112
+ return None
113
+ if not isinstance(value, bool):
114
+ raise ValueError(f"Config key '{key}' must be a boolean")
115
+ return value
116
+
117
+
118
+ def _expect_optional_str_list(value: Any, key: str) -> list[str] | None:
119
+ if value is None:
120
+ return None
121
+ if not isinstance(value, list) or not all(isinstance(v, str) for v in value):
122
+ raise ValueError(f"Config key '{key}' must be an array of strings")
123
+ return list(value)
124
+
125
+
126
+ def _validate_schema(data: dict[str, Any]) -> None:
127
+ allowed_top = {"project", "doctor", "dockerfile", "benchmark"}
128
+ unknown_top = sorted(set(data.keys()) - allowed_top)
129
+ if unknown_top:
130
+ raise ValueError(f"Unknown config section(s): {', '.join(unknown_top)}")
131
+
132
+ project = _expect_table(data, "project")
133
+ doctor = _expect_table(data, "doctor")
134
+ dockerfile = _expect_table(data, "dockerfile")
135
+ benchmark = _expect_table(data, "benchmark")
136
+ benchmark_run = benchmark.get("run", {})
137
+ benchmark_generate = benchmark.get("generate", {})
138
+ if benchmark_run and not isinstance(benchmark_run, dict):
139
+ raise ValueError("Config section 'benchmark.run' must be a TOML table")
140
+ if benchmark_generate and not isinstance(benchmark_generate, dict):
141
+ raise ValueError("Config section 'benchmark.generate' must be a TOML table")
142
+
143
+ for section_name, section, allowed_keys in [
144
+ ("project", project, {"build_tool"}),
145
+ ("doctor", doctor, {"build_tool"}),
146
+ (
147
+ "dockerfile",
148
+ dockerfile,
149
+ {"output", "java_version", "must_have_modules_file", "legacy_scripts", "wizard_args"},
150
+ ),
151
+ ("benchmark", benchmark, {"run", "generate", "profile", "runner_args"}),
152
+ (
153
+ "benchmark.run",
154
+ benchmark_run,
155
+ {"profile", "runner_args", "cpuset_cpus", "memory_limit", "warmup_runs", "max_workers", "normalized_runtime", "legacy_scripts"},
156
+ ),
157
+ ("benchmark.generate", benchmark_generate, {"java_version", "legacy_scripts"}),
158
+ ]:
159
+ unknown = sorted(set(section.keys()) - allowed_keys)
160
+ if unknown:
161
+ raise ValueError(f"Unknown config key(s) in [{section_name}]: {', '.join(unknown)}")
162
+
163
+ _expect_optional_str(project.get("build_tool"), "project.build_tool")
164
+ _expect_optional_str(doctor.get("build_tool"), "doctor.build_tool")
165
+ _expect_optional_str(dockerfile.get("output"), "dockerfile.output")
166
+ _expect_optional_int(dockerfile.get("java_version"), "dockerfile.java_version")
167
+ _expect_optional_str(dockerfile.get("must_have_modules_file"), "dockerfile.must_have_modules_file")
168
+ _expect_optional_bool(dockerfile.get("legacy_scripts"), "dockerfile.legacy_scripts")
169
+ _expect_optional_str_list(dockerfile.get("wizard_args"), "dockerfile.wizard_args")
170
+ _expect_optional_str(benchmark_run.get("profile"), "benchmark.run.profile")
171
+ _expect_optional_str_list(benchmark_run.get("runner_args"), "benchmark.run.runner_args")
172
+ _expect_optional_str(benchmark_run.get("cpuset_cpus"), "benchmark.run.cpuset_cpus")
173
+ _expect_optional_str(benchmark_run.get("memory_limit"), "benchmark.run.memory_limit")
174
+ _expect_optional_int(benchmark_run.get("warmup_runs"), "benchmark.run.warmup_runs")
175
+ _expect_optional_int(benchmark_run.get("max_workers"), "benchmark.run.max_workers")
176
+ _expect_optional_bool(benchmark_run.get("normalized_runtime"), "benchmark.run.normalized_runtime")
177
+ _expect_optional_bool(benchmark_run.get("legacy_scripts"), "benchmark.run.legacy_scripts")
178
+ _expect_optional_int(benchmark_generate.get("java_version"), "benchmark.generate.java_version")
179
+ _expect_optional_bool(benchmark_generate.get("legacy_scripts"), "benchmark.generate.legacy_scripts")
180
+
181
+ # Backward compatible legacy keys under [benchmark].
182
+ _expect_optional_str(benchmark.get("profile"), "benchmark.profile")
183
+ _expect_optional_str_list(benchmark.get("runner_args"), "benchmark.runner_args")
184
+
185
+
186
+ def load_config(path: Path, strict: bool = True) -> dict[str, Any]:
187
+ """Load TOML config file; return empty dict when file is absent."""
188
+ if not path.exists():
189
+ return {}
190
+ data = tomllib.loads(path.read_text(encoding="utf-8"))
191
+ if not isinstance(data, dict):
192
+ raise ValueError("Config root must be a TOML table")
193
+ if strict:
194
+ _validate_schema(data)
195
+ return data
196
+
197
+
198
+ def _resolve_build_tool(cli_build_tool: str | None, loaded_config: dict[str, Any], section: str) -> str | None:
199
+ value: str | None
200
+ if cli_build_tool is not None:
201
+ value = cli_build_tool
202
+ else:
203
+ target = _expect_table(loaded_config, section)
204
+ project = _expect_table(loaded_config, "project")
205
+ value = _expect_optional_str(target.get("build_tool"), f"{section}.build_tool") or _expect_optional_str(
206
+ project.get("build_tool"), "project.build_tool"
207
+ )
208
+ if value is not None and value not in {"maven", "gradle"}:
209
+ raise ValueError("build tool must be 'maven' or 'gradle'")
210
+ return value
211
+
212
+
213
+ def resolve_doctor_config(cli_build_tool: str | None, loaded_config: dict[str, Any]) -> DoctorConfig:
214
+ return DoctorConfig(build_tool=_resolve_build_tool(cli_build_tool, loaded_config, "doctor"))
215
+
216
+
217
+ def resolve_dockerfile_generate_config(
218
+ cli_build_tool: str | None,
219
+ cli_output: str | None,
220
+ cli_java_version: int | None,
221
+ cli_wizard_args: list[str] | None,
222
+ cli_use_legacy_scripts: bool | None,
223
+ loaded_config: dict[str, Any],
224
+ ) -> DockerfileGenerateConfig:
225
+ dockerfile = _expect_table(loaded_config, "dockerfile")
226
+ build_tool = _resolve_build_tool(cli_build_tool, loaded_config, "project")
227
+ output = cli_output or _expect_optional_str(dockerfile.get("output"), "dockerfile.output") or "Dockerfile.generated"
228
+ java_version = cli_java_version or _expect_optional_int(dockerfile.get("java_version"), "dockerfile.java_version") or 25
229
+ must_have_modules_file = _expect_optional_str(
230
+ dockerfile.get("must_have_modules_file"),
231
+ "dockerfile.must_have_modules_file",
232
+ )
233
+
234
+ if cli_wizard_args is not None:
235
+ wizard_args = cli_wizard_args
236
+ else:
237
+ wizard_args = _expect_optional_str_list(dockerfile.get("wizard_args"), "dockerfile.wizard_args") or []
238
+
239
+ if cli_use_legacy_scripts is not None:
240
+ use_legacy = cli_use_legacy_scripts
241
+ else:
242
+ use_legacy = _expect_optional_bool(dockerfile.get("legacy_scripts"), "dockerfile.legacy_scripts") or False
243
+
244
+ return DockerfileGenerateConfig(
245
+ build_tool=build_tool,
246
+ output=output,
247
+ java_version=java_version,
248
+ must_have_modules_file=must_have_modules_file,
249
+ wizard_args=wizard_args,
250
+ use_legacy_scripts=use_legacy,
251
+ )
252
+
253
+
254
+ def resolve_benchmark_generate_config(
255
+ cli_build_tool: str | None,
256
+ cli_java_version: int | None,
257
+ cli_use_legacy_scripts: bool | None,
258
+ loaded_config: dict[str, Any],
259
+ ) -> BenchmarkGenerateConfig:
260
+ benchmark = _expect_table(loaded_config, "benchmark")
261
+ generate = benchmark.get("generate", {})
262
+ if generate and not isinstance(generate, dict):
263
+ raise ValueError("Config section 'benchmark.generate' must be a TOML table")
264
+
265
+ build_tool = _resolve_build_tool(cli_build_tool, loaded_config, "project")
266
+ java_version = cli_java_version or _expect_optional_int(generate.get("java_version"), "benchmark.generate.java_version") or 25
267
+ if cli_use_legacy_scripts is not None:
268
+ use_legacy = cli_use_legacy_scripts
269
+ else:
270
+ use_legacy = _expect_optional_bool(generate.get("legacy_scripts"), "benchmark.generate.legacy_scripts") or False
271
+ return BenchmarkGenerateConfig(build_tool=build_tool, java_version=java_version, use_legacy_scripts=use_legacy)
272
+
273
+
274
+ def resolve_benchmark_run_config(
275
+ cli_build_tool: str | None,
276
+ cli_profile: str | None,
277
+ cli_runner_args: list[str] | None,
278
+ cli_cpuset_cpus: str | None,
279
+ cli_memory_limit: str | None,
280
+ cli_warmup_runs: int | None,
281
+ cli_max_workers: int | None,
282
+ cli_normalized_runtime: bool | None,
283
+ cli_use_legacy_scripts: bool | None,
284
+ loaded_config: dict[str, Any],
285
+ ) -> BenchmarkRunConfig:
286
+ """Merge benchmark-run settings with precedence: CLI > config > defaults."""
287
+ benchmark = _expect_table(loaded_config, "benchmark")
288
+ run_cfg = benchmark.get("run", {})
289
+ if run_cfg and not isinstance(run_cfg, dict):
290
+ raise ValueError("Config section 'benchmark.run' must be a TOML table")
291
+
292
+ build_tool = _resolve_build_tool(cli_build_tool, loaded_config, "project")
293
+ legacy_profile = _expect_optional_str(benchmark.get("profile"), "benchmark.profile")
294
+ legacy_runner_args = _expect_optional_str_list(benchmark.get("runner_args"), "benchmark.runner_args")
295
+ profile = cli_profile or _expect_optional_str(run_cfg.get("profile"), "benchmark.run.profile") or legacy_profile or "quick"
296
+ if profile not in {"quick", "full"}:
297
+ raise ValueError("benchmark profile must be 'quick' or 'full'")
298
+
299
+ if cli_runner_args is not None:
300
+ runner_args = cli_runner_args
301
+ else:
302
+ runner_args = (
303
+ _expect_optional_str_list(run_cfg.get("runner_args"), "benchmark.run.runner_args")
304
+ or legacy_runner_args
305
+ or []
306
+ )
307
+
308
+ cpuset_cpus: str | None
309
+ if cli_cpuset_cpus is not None:
310
+ cpuset_cpus = cli_cpuset_cpus
311
+ else:
312
+ cpuset_cpus = _expect_optional_str(run_cfg.get("cpuset_cpus"), "benchmark.run.cpuset_cpus")
313
+
314
+ memory_limit: str | None
315
+ if cli_memory_limit is not None:
316
+ memory_limit = cli_memory_limit
317
+ else:
318
+ memory_limit = _expect_optional_str(run_cfg.get("memory_limit"), "benchmark.run.memory_limit")
319
+
320
+ warmup_runs: int
321
+ if cli_warmup_runs is not None:
322
+ warmup_runs = cli_warmup_runs
323
+ else:
324
+ raw_warmup_runs = _expect_optional_int(run_cfg.get("warmup_runs"), "benchmark.run.warmup_runs")
325
+ warmup_runs = raw_warmup_runs if raw_warmup_runs is not None else 0
326
+ if warmup_runs < 0:
327
+ raise ValueError("benchmark.run.warmup_runs must be >= 0")
328
+
329
+ max_workers: int
330
+ if cli_max_workers is not None:
331
+ max_workers = cli_max_workers
332
+ else:
333
+ raw_max_workers = _expect_optional_int(run_cfg.get("max_workers"), "benchmark.run.max_workers")
334
+ max_workers = raw_max_workers if raw_max_workers is not None else 1
335
+ if max_workers < 1:
336
+ raise ValueError("benchmark.run.max_workers must be >= 1")
337
+
338
+ normalized_runtime: bool
339
+ if cli_normalized_runtime is not None:
340
+ normalized_runtime = cli_normalized_runtime
341
+ else:
342
+ raw_normalized_runtime = _expect_optional_bool(
343
+ run_cfg.get("normalized_runtime"),
344
+ "benchmark.run.normalized_runtime",
345
+ )
346
+ normalized_runtime = raw_normalized_runtime if raw_normalized_runtime is not None else False
347
+
348
+ use_legacy: bool
349
+ if cli_use_legacy_scripts is not None:
350
+ use_legacy = cli_use_legacy_scripts
351
+ else:
352
+ raw_use_legacy = _expect_optional_bool(run_cfg.get("legacy_scripts"), "benchmark.run.legacy_scripts")
353
+ use_legacy = raw_use_legacy if raw_use_legacy is not None else False
354
+
355
+ return BenchmarkRunConfig(
356
+ build_tool=build_tool,
357
+ profile=profile,
358
+ runner_args=runner_args,
359
+ cpuset_cpus=cpuset_cpus,
360
+ memory_limit=memory_limit,
361
+ warmup_runs=warmup_runs,
362
+ max_workers=max_workers,
363
+ normalized_runtime=normalized_runtime,
364
+ use_legacy_scripts=use_legacy,
365
+ )