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/__init__.py +9 -0
- springdocker/analyze.py +284 -0
- springdocker/benchmarks/__init__.py +2 -0
- springdocker/benchmarks/generate.py +104 -0
- springdocker/benchmarks/runner.py +343 -0
- springdocker/cli.py +289 -0
- springdocker/commands.py +388 -0
- springdocker/compare.py +138 -0
- springdocker/config.py +365 -0
- springdocker/dockerfile.py +332 -0
- springdocker/errors.py +16 -0
- springdocker/plugins.py +75 -0
- springdocker/project_detect.py +256 -0
- springdocker/regression.py +151 -0
- springdocker/services/__init__.py +2 -0
- springdocker/services/benchmark_service.py +124 -0
- springdocker/services/dockerfile_service.py +76 -0
- springdocker/services/project_service.py +37 -0
- springdocker-1.0.1.dist-info/METADATA +189 -0
- springdocker-1.0.1.dist-info/RECORD +23 -0
- springdocker-1.0.1.dist-info/WHEEL +5 -0
- springdocker-1.0.1.dist-info/entry_points.txt +2 -0
- springdocker-1.0.1.dist-info/top_level.txt +1 -0
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
|
+
)
|