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.
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from .analyze import VariantSummary
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class RegressionViolation:
12
+ scenario: str
13
+ variant: str
14
+ metric: str
15
+ baseline: float | None
16
+ current: float | None
17
+ delta_pct: float | None
18
+
19
+
20
+ METRICS = (
21
+ ("startup_avg_ms", "startup_avg_ms"),
22
+ ("startup_p95_ms", "startup_p95_ms"),
23
+ ("image_mb_avg", "image_mb_avg"),
24
+ )
25
+
26
+
27
+ def _metric_value(summary: VariantSummary, field: str) -> float | None:
28
+ return getattr(summary, field)
29
+
30
+
31
+ def _pct_change(current: float, baseline: float) -> float:
32
+ return ((current - baseline) / baseline) * 100.0
33
+
34
+
35
+ def load_summaries(path: Path) -> list[VariantSummary]:
36
+ payload = json.loads(path.read_text(encoding="utf-8"))
37
+ if not isinstance(payload, list):
38
+ raise ValueError("baseline report must be a JSON list")
39
+
40
+ summaries: list[VariantSummary] = []
41
+ for item in payload:
42
+ if not isinstance(item, dict):
43
+ raise ValueError("baseline report must contain JSON objects")
44
+ summaries.append(
45
+ VariantSummary(
46
+ scenario=str(item.get("scenario", "")),
47
+ variant=str(item.get("variant", "")),
48
+ runs=int(item.get("runs", 0)),
49
+ build_avg_ms=item.get("build_avg_ms"),
50
+ build_stddev_ms=item.get("build_stddev_ms"),
51
+ build_ci95_low_ms=item.get("build_ci95_low_ms"),
52
+ build_ci95_high_ms=item.get("build_ci95_high_ms"),
53
+ startup_avg_ms=item.get("startup_avg_ms"),
54
+ startup_p95_ms=item.get("startup_p95_ms"),
55
+ startup_p99_ms=item.get("startup_p99_ms"),
56
+ startup_stddev_ms=item.get("startup_stddev_ms"),
57
+ startup_ci95_low_ms=item.get("startup_ci95_low_ms"),
58
+ startup_ci95_high_ms=item.get("startup_ci95_high_ms"),
59
+ gc_pause_ms_avg=item.get("gc_pause_ms_avg"),
60
+ alloc_mb_avg=item.get("alloc_mb_avg"),
61
+ startup_phase_boot_ms_avg=item.get("startup_phase_boot_ms_avg"),
62
+ startup_phase_context_ms_avg=item.get("startup_phase_context_ms_avg"),
63
+ startup_phase_web_server_ms_avg=item.get("startup_phase_web_server_ms_avg"),
64
+ startup_phase_total_ms_avg=item.get("startup_phase_total_ms_avg"),
65
+ image_mb_avg=item.get("image_mb_avg"),
66
+ success_rate_pct=float(item.get("success_rate_pct", 0.0)),
67
+ rss_mb_avg=item.get("rss_mb_avg"),
68
+ cpu_pct_avg=item.get("cpu_pct_avg"),
69
+ host=item.get("host"),
70
+ docker_version=item.get("docker_version"),
71
+ run_profile=item.get("run_profile"),
72
+ )
73
+ )
74
+ return summaries
75
+
76
+
77
+ def detect_regressions(
78
+ baseline: list[VariantSummary], current: list[VariantSummary], threshold_pct: float
79
+ ) -> list[RegressionViolation]:
80
+ baseline_map = {(summary.scenario, summary.variant): summary for summary in baseline}
81
+ current_map = {(summary.scenario, summary.variant): summary for summary in current}
82
+ violations: list[RegressionViolation] = []
83
+
84
+ for key, baseline_summary in sorted(baseline_map.items()):
85
+ current_summary = current_map.get(key)
86
+ if current_summary is None:
87
+ continue
88
+ for metric_name, field in METRICS:
89
+ baseline_value = _metric_value(baseline_summary, field)
90
+ current_value = _metric_value(current_summary, field)
91
+ if baseline_value is None and current_value is None:
92
+ continue
93
+ if baseline_value is None and current_value is not None:
94
+ continue
95
+ if baseline_value is not None and current_value is None:
96
+ violations.append(
97
+ RegressionViolation(
98
+ scenario=baseline_summary.scenario,
99
+ variant=baseline_summary.variant,
100
+ metric=metric_name,
101
+ baseline=baseline_value,
102
+ current=None,
103
+ delta_pct=None,
104
+ )
105
+ )
106
+ continue
107
+ assert baseline_value is not None
108
+ assert current_value is not None
109
+ delta_pct = _pct_change(current_value, baseline_value)
110
+ if delta_pct > threshold_pct:
111
+ violations.append(
112
+ RegressionViolation(
113
+ scenario=baseline_summary.scenario,
114
+ variant=baseline_summary.variant,
115
+ metric=metric_name,
116
+ baseline=baseline_value,
117
+ current=current_value,
118
+ delta_pct=delta_pct,
119
+ )
120
+ )
121
+ return violations
122
+
123
+
124
+ def format_regression_table(violations: list[RegressionViolation]) -> str:
125
+ lines = [
126
+ "| Scenario | Variant | Metric | Baseline | Current | Δ% |",
127
+ "|---|---|---|---:|---:|---:|",
128
+ ]
129
+ for violation in violations:
130
+ baseline = "-" if violation.baseline is None else f"{violation.baseline:.2f}"
131
+ current = "-" if violation.current is None else f"{violation.current:.2f}"
132
+ delta = "-" if violation.delta_pct is None else f"{violation.delta_pct:+.1f}%"
133
+ lines.append(
134
+ f"| {violation.scenario} | {violation.variant} | {violation.metric} | {baseline} | {current} | {delta} |"
135
+ )
136
+ return "\n".join(lines)
137
+
138
+
139
+ def format_regression_json(violations: list[RegressionViolation]) -> str:
140
+ payload = [
141
+ {
142
+ "scenario": violation.scenario,
143
+ "variant": violation.variant,
144
+ "metric": violation.metric,
145
+ "baseline": violation.baseline,
146
+ "current": violation.current,
147
+ "delta_pct": violation.delta_pct,
148
+ }
149
+ for violation in violations
150
+ ]
151
+ return json.dumps(payload, indent=2, sort_keys=True)
@@ -0,0 +1,2 @@
1
+ from __future__ import annotations
2
+
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from ..analyze import format_json, format_table, summarize_csv
7
+ from ..compare import compare_summaries, format_delta_json, format_delta_table
8
+ from ..regression import RegressionViolation, detect_regressions, load_summaries
9
+
10
+
11
+ def resolve_path(project_root: Path, raw_path: str) -> Path:
12
+ path = Path(raw_path)
13
+ if not path.is_absolute():
14
+ path = project_root / path
15
+ return path
16
+
17
+
18
+ def validate_reproducibility_with_legacy(
19
+ use_legacy_scripts: bool,
20
+ cpuset_cpus: str | None,
21
+ memory_limit: str | None,
22
+ warmup_runs: int,
23
+ max_workers: int,
24
+ normalized_runtime: bool,
25
+ ) -> None:
26
+ if use_legacy_scripts and any([cpuset_cpus, memory_limit, warmup_runs > 0, max_workers > 1, normalized_runtime]):
27
+ raise ValueError("benchmark reproducibility/concurrency controls require the internal benchmark runner")
28
+
29
+
30
+ def render_comparison(
31
+ project_root: Path,
32
+ raw_csv: str,
33
+ baseline_variant: str,
34
+ output_format: str,
35
+ scenario: str | None,
36
+ ) -> str:
37
+ csv_path = resolve_path(project_root, raw_csv)
38
+ if not csv_path.exists():
39
+ raise ValueError(f"missing CSV file: {csv_path}")
40
+ summaries = summarize_csv(csv_path, scenario=scenario)
41
+ deltas = compare_summaries(baseline_variant, summaries)
42
+ return format_delta_json(deltas) if output_format == "json" else format_delta_table(deltas)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class AnalyzeOutcome:
47
+ rendered: str
48
+ output_destination: Path | None
49
+ success_rate_violations: tuple[str, ...]
50
+ regression_violations: tuple[RegressionViolation, ...]
51
+ baseline_missing: Path | None
52
+ baseline_path_used: Path | None
53
+ regression_threshold_pct: float
54
+
55
+
56
+ def analyze_csv(
57
+ project_root: Path,
58
+ raw_csv: str,
59
+ output_format: str,
60
+ scenario: str | None,
61
+ variant: str | None,
62
+ output_path: str | None,
63
+ fail_on_success_rate_below: float | None,
64
+ baseline_path: str | None,
65
+ fail_on_regression_above: float | None,
66
+ ) -> AnalyzeOutcome:
67
+ csv_path = resolve_path(project_root, raw_csv)
68
+ if not csv_path.exists():
69
+ raise ValueError(f"missing CSV file: {csv_path}")
70
+ if fail_on_success_rate_below is not None and not 0.0 <= fail_on_success_rate_below <= 100.0:
71
+ raise ValueError("--fail-on-success-rate-below must be between 0 and 100")
72
+
73
+ summaries = summarize_csv(csv_path, scenario=scenario, variant=variant)
74
+ if not summaries:
75
+ return AnalyzeOutcome(
76
+ rendered="No rows matched the provided filters.",
77
+ output_destination=None,
78
+ success_rate_violations=(),
79
+ regression_violations=(),
80
+ baseline_missing=None,
81
+ baseline_path_used=None,
82
+ regression_threshold_pct=fail_on_regression_above or 20.0,
83
+ )
84
+
85
+ rendered = format_json(summaries) if output_format == "json" else format_table(summaries)
86
+ destination: Path | None = None
87
+ if output_path:
88
+ destination = resolve_path(project_root, output_path)
89
+
90
+ violations: list[str] = []
91
+ if fail_on_success_rate_below is not None:
92
+ violations = [
93
+ f"success_rate below threshold for {summary.scenario}/{summary.variant}: "
94
+ f"{summary.success_rate_pct:.1f}% < {fail_on_success_rate_below:.1f}%"
95
+ for summary in summaries
96
+ if summary.success_rate_pct < fail_on_success_rate_below
97
+ ]
98
+
99
+ baseline_missing: Path | None = None
100
+ baseline_path_used: Path | None = None
101
+ regression_violations: list[RegressionViolation] = []
102
+ threshold = fail_on_regression_above or 20.0
103
+ if baseline_path is not None:
104
+ baseline_file = resolve_path(project_root, baseline_path)
105
+ baseline_path_used = baseline_file
106
+ if not baseline_file.exists():
107
+ baseline_missing = baseline_file
108
+ else:
109
+ baseline_summaries = load_summaries(baseline_file)
110
+ regression_violations = detect_regressions(
111
+ baseline=baseline_summaries,
112
+ current=summaries,
113
+ threshold_pct=threshold,
114
+ )
115
+
116
+ return AnalyzeOutcome(
117
+ rendered=rendered,
118
+ output_destination=destination,
119
+ success_rate_violations=tuple(violations),
120
+ regression_violations=tuple(regression_violations),
121
+ baseline_missing=baseline_missing,
122
+ baseline_path_used=baseline_path_used,
123
+ regression_threshold_pct=threshold,
124
+ )
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from ..dockerfile import DockerfileOptions, build_dockerfile, explain_dockerfile_text
8
+ from ..plugins import apply_dockerfile_mutators
9
+
10
+
11
+ def resolve_path(project_root: Path, raw_path: str) -> Path:
12
+ path = Path(raw_path)
13
+ if not path.is_absolute():
14
+ path = project_root / path
15
+ return path
16
+
17
+
18
+ def parse_must_have_modules(project_root: Path, must_have_modules_file: str | None) -> tuple[str, ...]:
19
+ if not must_have_modules_file:
20
+ return ()
21
+ modules_path = resolve_path(project_root, must_have_modules_file)
22
+ if not modules_path.exists():
23
+ raise ValueError(f"missing must-have modules file: {modules_path}")
24
+ parsed: list[str] = []
25
+ seen: set[str] = set()
26
+ for line in modules_path.read_text(encoding="utf-8").splitlines():
27
+ entry = line.split("#", 1)[0].strip()
28
+ if not entry:
29
+ continue
30
+ for token in [part.strip() for part in entry.split(",")]:
31
+ if not token:
32
+ continue
33
+ if not re.fullmatch(r"[A-Za-z0-9._-]+", token):
34
+ raise ValueError(f"invalid module name in {modules_path}: {token}")
35
+ if token not in seen:
36
+ parsed.append(token)
37
+ seen.add(token)
38
+ return tuple(parsed)
39
+
40
+
41
+ def generate_dockerfile(
42
+ project_root: Path,
43
+ output_path: str,
44
+ build_tool: str,
45
+ java_version: int,
46
+ must_have_modules_file: str | None,
47
+ ) -> GeneratedDockerfile:
48
+ must_have_modules = parse_must_have_modules(project_root, must_have_modules_file)
49
+ options = DockerfileOptions(
50
+ build_tool=build_tool,
51
+ java_version=java_version,
52
+ must_have_modules=must_have_modules,
53
+ )
54
+ generated = apply_dockerfile_mutators(
55
+ dockerfile_text=build_dockerfile(options),
56
+ options=options,
57
+ )
58
+ destination = resolve_path(project_root, output_path)
59
+ destination.parent.mkdir(parents=True, exist_ok=True)
60
+ destination.write_text(generated.dockerfile_text, encoding="utf-8")
61
+ return GeneratedDockerfile(path=destination, plugin_warnings=generated.warnings)
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class GeneratedDockerfile:
66
+ path: Path
67
+ plugin_warnings: tuple[str, ...]
68
+
69
+
70
+ def explain_dockerfile(project_root: Path, dockerfile_path: str) -> dict[str, object]:
71
+ path = resolve_path(project_root, dockerfile_path)
72
+ if not path.exists():
73
+ raise ValueError(f"missing Dockerfile: {path}")
74
+ payload = dict(explain_dockerfile_text(path.read_text(encoding="utf-8")))
75
+ payload["path"] = str(path)
76
+ return payload
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from ..config import render_default_config, write_default_config
7
+ from ..project_detect import inspect_project, inspect_project_details
8
+
9
+
10
+ def load_project_info(project_root: Path, build_tool: str | None):
11
+ return inspect_project(project_root, build_tool)
12
+
13
+
14
+ def load_project_details(project_root: Path, build_tool: str | None):
15
+ return inspect_project_details(project_root, build_tool)
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class InitConfigResult:
20
+ rendered: str | None
21
+ written_path: Path | None
22
+
23
+
24
+ def prepare_default_config(
25
+ project_root: Path,
26
+ build_tool: str | None,
27
+ config_path: Path,
28
+ profile: str,
29
+ force: bool,
30
+ print_only: bool,
31
+ ) -> InitConfigResult:
32
+ info = inspect_project(project_root, build_tool)
33
+ if print_only:
34
+ return InitConfigResult(rendered=render_default_config(build_tool=info.build_tool, profile=profile), written_path=None)
35
+ write_default_config(path=config_path, build_tool=info.build_tool, profile=profile, force=force)
36
+ return InitConfigResult(rendered=None, written_path=config_path)
37
+
@@ -0,0 +1,189 @@
1
+ Metadata-Version: 2.4
2
+ Name: springdocker
3
+ Version: 1.0.1
4
+ Summary: CLI for Spring Boot Dockerfile and benchmark workflows (Maven/Gradle).
5
+ Author: springdocker contributors
6
+ License: UNLICENSED
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: tomli>=2.0; python_version < "3.11"
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0; extra == "dev"
12
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
13
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
14
+ Requires-Dist: mypy>=1.10; extra == "dev"
15
+
16
+ # springdocker CLI
17
+
18
+ CLI for Spring Boot Dockerfile and benchmark workflows across Maven and Gradle projects.
19
+
20
+ ## Install
21
+
22
+ ### Local editable
23
+
24
+ ```bash
25
+ python3 -m pip install -e .
26
+ ```
27
+
28
+ ### pipx
29
+
30
+ ```bash
31
+ pipx install springdocker
32
+ springdocker --help
33
+ ```
34
+
35
+ Upgrade:
36
+
37
+ ```bash
38
+ pipx upgrade springdocker
39
+ ```
40
+
41
+ ### uv
42
+
43
+ ```bash
44
+ uv tool install springdocker
45
+ uv tool upgrade springdocker
46
+ ```
47
+
48
+ ## Quick usage
49
+
50
+ ```bash
51
+ springdocker init --project-root samples/java-spring-docker --build-tool maven --profile quick
52
+ springdocker doctor --project-root samples/java-spring-docker
53
+ springdocker inspect --project-root samples/java-spring-docker --format json
54
+ springdocker explain --project-root samples/java-spring-docker Dockerfile.generated --format json
55
+ springdocker benchmark compare --project-root samples/java-spring-docker benchmarks/03-custom-jre-jlink/results/raw.csv --baseline-variant with-jlink-runtime --format json
56
+ springdocker dockerfile generate --project-root samples/java-spring-docker --output Dockerfile.generated
57
+ springdocker benchmark generate --project-root samples/java-spring-docker --java-version 25
58
+ springdocker benchmark run --project-root samples/java-spring-docker --profile quick --runner-arg --skip-native
59
+ springdocker benchmark analyze --project-root samples/java-spring-docker benchmarks/04-jep483-aot-cache/results/raw.csv --format table
60
+ springdocker benchmark analyze --project-root samples/java-spring-docker benchmarks/04-jep483-aot-cache/results/raw.csv --format json --output benchmarks/04-jep483-aot-cache/results/summary.json
61
+ springdocker benchmark analyze --project-root samples/java-spring-docker benchmarks/04-jep483-aot-cache/results/raw.csv --fail-on-success-rate-below 95
62
+ springdocker benchmark analyze --project-root samples/java-spring-docker benchmarks/04-jep483-aot-cache/results/raw.csv --baseline benchmarks/04-jep483-aot-cache/results/baseline.json --fail-on-regression-above 20
63
+ ```
64
+
65
+ ## Config file (`.springdocker.toml`)
66
+
67
+ All command resolvers use precedence:
68
+
69
+ 1. CLI flags
70
+ 2. `.springdocker.toml`
71
+ 3. defaults
72
+
73
+ Example:
74
+
75
+ ```toml
76
+ [project]
77
+ build_tool = "maven"
78
+
79
+ [doctor]
80
+ build_tool = "maven"
81
+
82
+ [dockerfile]
83
+ output = "Dockerfile.generated"
84
+ java_version = 25
85
+ must_have_modules_file = "must-have.txt"
86
+ legacy_scripts = false
87
+ wizard_args = []
88
+
89
+ [benchmark.generate]
90
+ java_version = 25
91
+ legacy_scripts = false
92
+
93
+ [benchmark.run]
94
+ profile = "quick"
95
+ runner_args = ["--skip-native"]
96
+ cpuset_cpus = "0-1"
97
+ memory_limit = "2g"
98
+ warmup_runs = 1
99
+ max_workers = 1
100
+ normalized_runtime = true
101
+ legacy_scripts = false
102
+ ```
103
+
104
+ When `dockerfile.must_have_modules_file` is set, springdocker reads modules from that file
105
+ (`must-have.txt` style, one module per line, `#` comments allowed) and injects them into
106
+ the jlink module list for reflection/dynamic-loading edge cases.
107
+
108
+ Create template config:
109
+
110
+ ```bash
111
+ springdocker init --project-root samples/java-spring-docker --build-tool gradle
112
+ springdocker init --project-root samples/java-spring-docker --build-tool gradle --profile full --print
113
+ ```
114
+
115
+ ## Legacy compatibility mode
116
+
117
+ Main command paths are internal and do not require project script files.
118
+
119
+ To force script wrappers for compatibility:
120
+
121
+ ```bash
122
+ springdocker dockerfile generate --use-legacy-scripts ...
123
+ springdocker benchmark generate --use-legacy-scripts ...
124
+ springdocker benchmark run --use-legacy-scripts ...
125
+ ```
126
+
127
+ or set:
128
+
129
+ ```bash
130
+ export SPRINGDOCKER_LEGACY_SCRIPTS=1
131
+ ```
132
+
133
+ ## Inspect command
134
+
135
+ `springdocker inspect` prints static metadata about the target project:
136
+
137
+ - detected build tool
138
+ - Spring Boot version when present
139
+ - Java version when present
140
+ - direct dependency coordinates
141
+ - generated Dockerfile artifacts in the project root
142
+ - basic runtime compatibility guidance
143
+
144
+ Use `--format json` for machine-readable output.
145
+
146
+ ## Explain command
147
+
148
+ `springdocker explain` reads a springdocker-generated Dockerfile and describes the optimizations it contains:
149
+
150
+ - multi-stage layout
151
+ - BuildKit cache usage
152
+ - jlink runtime stage
153
+ - non-root runtime
154
+ - tuned JVM flags
155
+ - curated must-have modules
156
+
157
+ Use `--format json` when you want stable structured output.
158
+
159
+ ## Security hardening
160
+
161
+ See `docs/security-hardening.md` for the runtime hardening defaults and recommended `docker run` flags.
162
+
163
+ ## Binary distribution
164
+
165
+ See `docs/distribution.md` for packaging notes and sample Homebrew, Scoop, standalone binary, and Docker runtime artifacts.
166
+
167
+ ## Multi-architecture builds
168
+
169
+ See `docs/multiarch.md` for the Buildx-friendly Dockerfile output and example multi-arch build command.
170
+
171
+ ## Compare command
172
+
173
+ `springdocker benchmark compare` compares each variant against a required baseline variant and reports deltas.
174
+
175
+ - `--baseline-variant` selects the variant to compare against.
176
+ - `--scenario` narrows the CSV to one scenario.
177
+ - `--format json` produces machine-readable deltas.
178
+
179
+ ## Benchmark run reproducibility
180
+
181
+ `springdocker benchmark run` supports deterministic benchmark controls for local or CI runs:
182
+
183
+ - `--cpuset-cpus` pins benchmark containers to specific CPUs.
184
+ - `--memory` caps container memory.
185
+ - `--warmup-runs` executes discarded warmup probes before recording results.
186
+ - `--max-workers` runs standard scenarios concurrently with controlled worker count.
187
+ - `--normalized-runtime` applies read-only, no-new-privileges, and tmpfs isolation.
188
+
189
+ These settings can also come from `[benchmark.run]` in `.springdocker.toml`.
@@ -0,0 +1,23 @@
1
+ springdocker/__init__.py,sha256=33B3aRpZXrsoZ7GLyStF7MzVgIHt47MlR_EhxEjQZ9E,272
2
+ springdocker/analyze.py,sha256=z_j29739dZF_R4T7jwakTWgOMfuelQEwkrkVC0jDDQw,11992
3
+ springdocker/cli.py,sha256=3uVF0NByriJLPwvVVrxD-e9XsZmOYT0sa7EhFzD3RcE,12315
4
+ springdocker/commands.py,sha256=QBvyjPAt7y7iAVA226dBNxgJNZh9-WWvRPMkZ4JDcPA,12397
5
+ springdocker/compare.py,sha256=dK-h7hKEJoI8GlKmd8AZAV5--5SlZ7_C_kW0ighXooY,4931
6
+ springdocker/config.py,sha256=t9n5a8FUMETHO0r_udeYHUvNODAlxu1xXJN4l6I34cA,14660
7
+ springdocker/dockerfile.py,sha256=9Ov5vigs31AFe8LZOT_eyXRw1JSIVt6mwR2CDoqS49M,12504
8
+ springdocker/errors.py,sha256=nwBquBupwIB9VTWeyrtDlKXVXqOmvpDe6-oEUsFPfo8,275
9
+ springdocker/plugins.py,sha256=7uGnMRl4Dah_isi8SyLfELlUmDh4HWfNG6J5ky77mFg,2857
10
+ springdocker/project_detect.py,sha256=cih0zh9ZVZgtzfcXNeJZ7EwA7GDSkCZbI7EqSI1nr-8,9189
11
+ springdocker/regression.py,sha256=gOaPs0Fabs_-RGlp7xECBpXYL-jGNoSLKLCftK2t-Oc,6003
12
+ springdocker/benchmarks/__init__.py,sha256=WiQrmreDaPt7F3PhIkZQKaeYhegrolZEaL589su99sI,51
13
+ springdocker/benchmarks/generate.py,sha256=pHw5QrusKBqncgSdU1mCIoiaWS5KpiXKqdkZVW6oQmA,4069
14
+ springdocker/benchmarks/runner.py,sha256=AjEErI6ysN3aDXAEfM7wEXdP8gPp-CupVvCq_FboxTU,12210
15
+ springdocker/services/__init__.py,sha256=PHh4jqD7i0aL8W8hXThczKcT4xsNZiXanBxOH5IOW5I,36
16
+ springdocker/services/benchmark_service.py,sha256=Skj9x5w6Ix6rGVuTJWmeWPhx7qtvzI3SdYW5h2ElMsI,4480
17
+ springdocker/services/dockerfile_service.py,sha256=S1z1EkKePTqA96zaTRfAb7z59Z0BkrQzHah3MASPPzU,2630
18
+ springdocker/services/project_service.py,sha256=A0iUVyNvcHRDdq2khSvLk_MU7CSHuGdn0PRfiFKFUFA,1147
19
+ springdocker-1.0.1.dist-info/METADATA,sha256=EK3D1tOBmHU5eQijJRqa8cO8Az1WWM-EwN12YWhMUNI,5818
20
+ springdocker-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
21
+ springdocker-1.0.1.dist-info/entry_points.txt,sha256=KthNZ57ZMW6pFk6BMXuKpJYp6lOjHAebaDv5nSkvIOU,55
22
+ springdocker-1.0.1.dist-info/top_level.txt,sha256=7HhuHYUI1oB5IcfzRfIoX4D6jld22B-NWtvDPcOnneU,13
23
+ springdocker-1.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ springdocker = springdocker.cli:main
@@ -0,0 +1 @@
1
+ springdocker