eigenhelm 0.1.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.
Files changed (49) hide show
  1. eigenhelm/__init__.py +29 -0
  2. eigenhelm/cli/__init__.py +1 -0
  3. eigenhelm/cli/evaluate.py +260 -0
  4. eigenhelm/cli/harness.py +120 -0
  5. eigenhelm/cli/inspect.py +86 -0
  6. eigenhelm/cli/serve.py +76 -0
  7. eigenhelm/cli/train.py +134 -0
  8. eigenhelm/critic/__init__.py +165 -0
  9. eigenhelm/critic/aesthetic_critic.py +254 -0
  10. eigenhelm/critic/birkhoff.py +28 -0
  11. eigenhelm/critic/compression.py +31 -0
  12. eigenhelm/critic/entropy.py +30 -0
  13. eigenhelm/eigenspace/__init__.py +96 -0
  14. eigenhelm/eigenspace/projection.py +93 -0
  15. eigenhelm/harness/__init__.py +6 -0
  16. eigenhelm/harness/report.py +43 -0
  17. eigenhelm/harness/runner.py +140 -0
  18. eigenhelm/helm/__init__.py +137 -0
  19. eigenhelm/helm/dynamic_helm.py +182 -0
  20. eigenhelm/helm/models.py +111 -0
  21. eigenhelm/helm/pid.py +72 -0
  22. eigenhelm/metrics/__init__.py +1 -0
  23. eigenhelm/metrics/cyclomatic.py +55 -0
  24. eigenhelm/metrics/halstead.py +116 -0
  25. eigenhelm/metrics/wl_hash.py +102 -0
  26. eigenhelm/models.py +151 -0
  27. eigenhelm/parsers/__init__.py +1 -0
  28. eigenhelm/parsers/language_map.py +100 -0
  29. eigenhelm/parsers/tree_sitter.py +209 -0
  30. eigenhelm/serve/__init__.py +5 -0
  31. eigenhelm/serve/app.py +89 -0
  32. eigenhelm/serve/middleware/__init__.py +1 -0
  33. eigenhelm/serve/middleware/size_limit.py +89 -0
  34. eigenhelm/serve/middleware/timeout.py +61 -0
  35. eigenhelm/serve/models.py +82 -0
  36. eigenhelm/serve/routes/__init__.py +1 -0
  37. eigenhelm/serve/routes/evaluate.py +115 -0
  38. eigenhelm/serve/routes/health.py +36 -0
  39. eigenhelm/training/__init__.py +195 -0
  40. eigenhelm/training/corpus.py +75 -0
  41. eigenhelm/training/pca.py +85 -0
  42. eigenhelm/training/serialization.py +41 -0
  43. eigenhelm/virtue_extractor.py +232 -0
  44. eigenhelm-0.1.0.dist-info/METADATA +221 -0
  45. eigenhelm-0.1.0.dist-info/RECORD +49 -0
  46. eigenhelm-0.1.0.dist-info/WHEEL +5 -0
  47. eigenhelm-0.1.0.dist-info/entry_points.txt +6 -0
  48. eigenhelm-0.1.0.dist-info/licenses/LICENSE +618 -0
  49. eigenhelm-0.1.0.dist-info/top_level.txt +1 -0
eigenhelm/__init__.py ADDED
@@ -0,0 +1,29 @@
1
+ """Eigenhelm — Software Poet Conscience.
2
+
3
+ Stage 1: IVirtueExtractor
4
+ Language-agnostic code metric extraction producing 69-dimensional feature vectors.
5
+ """
6
+
7
+ from eigenhelm.models import (
8
+ CodeUnit,
9
+ CyclomaticMetrics,
10
+ EigenspaceModel,
11
+ FeatureVector,
12
+ HalsteadMetrics,
13
+ ProjectionResult,
14
+ TrainingResult,
15
+ UnsupportedLanguageError,
16
+ )
17
+ from eigenhelm.virtue_extractor import VirtueExtractor
18
+
19
+ __all__ = [
20
+ "VirtueExtractor",
21
+ "CodeUnit",
22
+ "FeatureVector",
23
+ "HalsteadMetrics",
24
+ "CyclomaticMetrics",
25
+ "EigenspaceModel",
26
+ "ProjectionResult",
27
+ "TrainingResult",
28
+ "UnsupportedLanguageError",
29
+ ]
@@ -0,0 +1 @@
1
+ """CLI entry points for eigenhelm-train and eigenhelm-inspect."""
@@ -0,0 +1,260 @@
1
+ """eigenhelm-evaluate CLI — evaluate files in-process.
2
+
3
+ Usage:
4
+ eigenhelm-evaluate path/to/file.py
5
+ eigenhelm-evaluate src/ --model model.npz
6
+ echo "def f(): pass" | eigenhelm-evaluate --language python
7
+ eigenhelm-evaluate src/ --json
8
+
9
+ Exit codes:
10
+ 0 All files accepted or warned
11
+ 1 One or more files rejected
12
+ 2 Runtime error
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ from eigenhelm.helm import DynamicHelm
24
+ from eigenhelm.helm.models import EvaluationRequest, EvaluationResponse
25
+ from eigenhelm.parsers.language_map import LANGUAGE_MAP
26
+
27
+ # Build extension → language mapping from LANGUAGE_MAP
28
+ EXTENSION_TO_LANGUAGE: dict[str, str] = {ext: lang for lang, (_, ext) in LANGUAGE_MAP.items()}
29
+
30
+
31
+ DEFAULT_EXCLUDES = {".git", "__pycache__", ".venv", "venv", "env", "node_modules", ".pytest_cache", "dist", "build"}
32
+
33
+ def discover_files(paths: list[Path]) -> list[tuple[Path, str]]:
34
+ """Discover eligible source files from given paths.
35
+
36
+ For files: looks up language via extension, skips unrecognized.
37
+ For directories: walks recursively, filtering out excluded directories.
38
+ Does NOT follow symlinks.
39
+
40
+ Returns (path, language) pairs sorted by path.
41
+ """
42
+ results: list[tuple[Path, str]] = []
43
+ for p in paths:
44
+ if p.is_file():
45
+ lang = EXTENSION_TO_LANGUAGE.get(p.suffix)
46
+ if lang is None:
47
+ print(f"WARNING: Skipping {p} (unrecognized extension)", file=sys.stderr)
48
+ continue
49
+ results.append((p, lang))
50
+ elif p.is_dir():
51
+ excludes = set(DEFAULT_EXCLUDES)
52
+ ignore_file = p / ".eigenhelmignore"
53
+ if ignore_file.is_file():
54
+ try:
55
+ custom_excludes = [
56
+ line.strip() for line in ignore_file.read_text().splitlines()
57
+ if line.strip() and not line.startswith("#")
58
+ ]
59
+ excludes.update(custom_excludes)
60
+ except OSError:
61
+ pass
62
+
63
+ for root, dirs, files in os.walk(p, followlinks=False):
64
+ # Prune excluded directories
65
+ retained = [d for d in dirs if d not in excludes and not d.endswith(".egg-info")]
66
+ dirs.clear()
67
+ dirs.extend(retained)
68
+
69
+ for filename in files:
70
+ if filename in excludes:
71
+ continue
72
+ child = Path(root) / filename
73
+ try:
74
+ if child.is_symlink() or not child.is_file():
75
+ continue
76
+ lang = EXTENSION_TO_LANGUAGE.get(child.suffix)
77
+ if lang is not None:
78
+ results.append((child, lang))
79
+ except OSError:
80
+ continue
81
+
82
+ return sorted(results, key=lambda x: x[0])
83
+
84
+
85
+ def format_result_human(path: Path | str, response: EvaluationResponse) -> str:
86
+ """Format a single result for human-readable output."""
87
+ lines = [
88
+ f"{path}",
89
+ f" decision: {response.decision}",
90
+ f" score: {response.score:.2f}",
91
+ f" confidence: {response.structural_confidence}",
92
+ ]
93
+ if response.critique.violations:
94
+ lines.append(" violations:")
95
+ for v in response.critique.violations:
96
+ pct = v.contribution * 100
97
+ lines.append(f" {v.dimension} (contribution: {pct:.0f}%)")
98
+ if response.warning:
99
+ lines.append(f" warning: {response.warning}")
100
+ return "\n".join(lines)
101
+
102
+
103
+ def format_summary_human(
104
+ results: list[tuple[Path | str, EvaluationResponse]],
105
+ ) -> str:
106
+ """Format aggregate summary for human-readable output."""
107
+ total = len(results)
108
+ accepted = sum(1 for _, r in results if r.decision == "accept")
109
+ warned = sum(1 for _, r in results if r.decision == "warn")
110
+ rejected = sum(1 for _, r in results if r.decision == "reject")
111
+ mean_score = sum(r.score for _, r in results) / total if total else 0.0
112
+ sep = "─" * 40
113
+ return (
114
+ f"{sep}\n"
115
+ f"Summary: {total} files | {accepted} accepted | {warned} warned | "
116
+ f"{rejected} rejected | mean score: {mean_score:.2f}"
117
+ )
118
+
119
+
120
+ def format_results_json(
121
+ results: list[tuple[Path | str, EvaluationResponse]],
122
+ ) -> str:
123
+ """Format results as JSON matching BatchResponse schema."""
124
+ result_dicts = []
125
+ for path, resp in results:
126
+ violations = [
127
+ {
128
+ "dimension": v.dimension,
129
+ "raw_value": v.raw_value,
130
+ "normalized_value": v.normalized_value,
131
+ "contribution": v.contribution,
132
+ }
133
+ for v in resp.critique.violations
134
+ ]
135
+ result_dicts.append(
136
+ {
137
+ "decision": resp.decision,
138
+ "score": resp.score,
139
+ "structural_confidence": resp.structural_confidence,
140
+ "violations": violations,
141
+ "warning": resp.warning,
142
+ "file_path": str(path),
143
+ }
144
+ )
145
+
146
+ total = len(result_dicts)
147
+ accepted = sum(1 for r in result_dicts if r["decision"] == "accept")
148
+ warned = sum(1 for r in result_dicts if r["decision"] == "warn")
149
+ rejected = sum(1 for r in result_dicts if r["decision"] == "reject")
150
+ mean_score = sum(r["score"] for r in result_dicts) / total if total else 0.0
151
+
152
+ if rejected > 0:
153
+ overall = "reject"
154
+ elif warned > 0:
155
+ overall = "warn"
156
+ else:
157
+ overall = "accept"
158
+
159
+ output = {
160
+ "results": result_dicts,
161
+ "summary": {
162
+ "overall_decision": overall,
163
+ "total_files": total,
164
+ "accepted": accepted,
165
+ "warned": warned,
166
+ "rejected": rejected,
167
+ "mean_score": mean_score,
168
+ },
169
+ }
170
+ return json.dumps(output, indent=2)
171
+
172
+
173
+ def _evaluate_stdin(
174
+ helm: DynamicHelm,
175
+ language: str,
176
+ ) -> list[tuple[Path | str, EvaluationResponse]]:
177
+ """Read from stdin and evaluate."""
178
+ source = sys.stdin.read()
179
+ resp = helm.evaluate(EvaluationRequest(source=source, language=language))
180
+ return [("<stdin>", resp)]
181
+
182
+
183
+ def _evaluate_paths(
184
+ helm: DynamicHelm,
185
+ paths: list[Path],
186
+ ) -> list[tuple[Path | str, EvaluationResponse]]:
187
+ """Discover and evaluate files from given paths."""
188
+ files = discover_files(paths)
189
+ results: list[tuple[Path | str, EvaluationResponse]] = []
190
+ for path, lang in files:
191
+ try:
192
+ source = path.read_text(encoding="utf-8")
193
+ except UnicodeDecodeError:
194
+ print(f"WARNING: Skipping binary file {path}", file=sys.stderr)
195
+ continue
196
+ resp = helm.evaluate(EvaluationRequest(source=source, language=lang, file_path=str(path)))
197
+ results.append((path, resp))
198
+ return results
199
+
200
+
201
+ def main(argv: list[str] | None = None) -> int:
202
+ """Entry point for eigenhelm-evaluate.
203
+
204
+ Returns exit code: 0 (no rejections), 1 (any rejection), 2 (runtime error).
205
+ """
206
+ parser = argparse.ArgumentParser(
207
+ prog="eigenhelm-evaluate",
208
+ description="Evaluate code files against the aesthetic manifold",
209
+ )
210
+ parser.add_argument("paths", nargs="*", type=Path, help="File or directory paths")
211
+ parser.add_argument(
212
+ "--language",
213
+ default=None,
214
+ help="Language (required for stdin mode when no paths given)",
215
+ )
216
+ parser.add_argument("--model", default=None, help="Path to .npz eigenspace model")
217
+ parser.add_argument("--json", dest="json_output", action="store_true", help="JSON output")
218
+ args = parser.parse_args(argv)
219
+
220
+ try:
221
+ eigenspace = None
222
+ if args.model:
223
+ from eigenhelm.eigenspace import load_model
224
+
225
+ eigenspace = load_model(args.model)
226
+
227
+ helm = DynamicHelm(eigenspace=eigenspace)
228
+
229
+ if not args.paths:
230
+ if not args.language:
231
+ print(
232
+ "ERROR: --language is required when reading from stdin",
233
+ file=sys.stderr,
234
+ )
235
+ return 2
236
+ results = _evaluate_stdin(helm, args.language)
237
+ else:
238
+ results = _evaluate_paths(helm, args.paths)
239
+
240
+ if not results:
241
+ return 0
242
+
243
+ if args.json_output:
244
+ print(format_results_json(results))
245
+ else:
246
+ for path, resp in results:
247
+ print(format_result_human(path, resp))
248
+ if len(results) > 1:
249
+ print(format_summary_human(results))
250
+
251
+ has_rejection = any(r.decision == "reject" for _, r in results)
252
+ return 1 if has_rejection else 0
253
+
254
+ except Exception as exc:
255
+ print(f"ERROR: {exc}", file=sys.stderr)
256
+ return 2
257
+
258
+
259
+ if __name__ == "__main__":
260
+ sys.exit(main())
@@ -0,0 +1,120 @@
1
+ """eigenhelm-harness CLI — compare two corpora with Mann-Whitney U test.
2
+
3
+ Usage:
4
+ eigenhelm-harness --before corpus/before/ --after corpus/after/
5
+ eigenhelm-harness --before before/ --after after/ --model model.npz --json
6
+
7
+ Exit codes:
8
+ 0 Harness completed (regardless of significance)
9
+ 1 One or both corpus directories empty/unreadable
10
+ 2 Runtime error
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ from eigenhelm.harness.report import HarnessReport
21
+
22
+
23
+ def format_harness_human(report: HarnessReport) -> str:
24
+ """Format a HarnessReport for human-readable output."""
25
+ b = report.before
26
+ a = report.after
27
+
28
+ delta_symbol = "✓ Improvement" if report.improvement else "✗ No improvement"
29
+ sig_symbol = "YES ✓" if report.significant else "NO ✗"
30
+
31
+ lines = [
32
+ "eigenhelm-harness: Quality Comparison Report",
33
+ f" Before corpus: {b.n_files} files evaluated, {b.n_skipped} skipped",
34
+ f" Mean score: {b.mean_score:.2f} "
35
+ f"(median: {b.median_score:.2f}, std: {b.std_score:.2f})",
36
+ f" Accept/Warn/Reject: {b.accepted} / {b.warned} / {b.rejected}",
37
+ "",
38
+ f" After corpus: {a.n_files} files evaluated, {a.n_skipped} skipped",
39
+ f" Mean score: {a.mean_score:.2f} "
40
+ f"(median: {a.median_score:.2f}, std: {a.std_score:.2f})",
41
+ f" Accept/Warn/Reject: {a.accepted} / {a.warned} / {a.rejected}",
42
+ "",
43
+ f" Delta (after − before): {report.delta_mean_score:+.2f} {delta_symbol}",
44
+ f" Mann-Whitney U: {report.u_statistic:.1f}",
45
+ f" p-value: {report.p_value:.4f}",
46
+ f" Significant at α=0.05: {sig_symbol}",
47
+ ]
48
+ return "\n".join(lines)
49
+
50
+
51
+ def format_harness_json(report: HarnessReport) -> str:
52
+ """Format a HarnessReport as JSON matching field names exactly."""
53
+
54
+ def _stats_dict(s):
55
+ return {
56
+ "n_files": s.n_files,
57
+ "n_skipped": s.n_skipped,
58
+ "mean_score": s.mean_score,
59
+ "median_score": s.median_score,
60
+ "std_score": s.std_score,
61
+ "accepted": s.accepted,
62
+ "warned": s.warned,
63
+ "rejected": s.rejected,
64
+ }
65
+
66
+ output = {
67
+ "before": _stats_dict(report.before),
68
+ "after": _stats_dict(report.after),
69
+ "delta_mean_score": report.delta_mean_score,
70
+ "u_statistic": report.u_statistic,
71
+ "p_value": report.p_value,
72
+ "significant": report.significant,
73
+ "improvement": report.improvement,
74
+ }
75
+ return json.dumps(output, indent=2)
76
+
77
+
78
+ def main(argv: list[str] | None = None) -> int:
79
+ """Entry point for eigenhelm-harness.
80
+
81
+ Returns exit code: 0 (success), 1 (empty corpus), 2 (runtime error).
82
+ """
83
+ parser = argparse.ArgumentParser(
84
+ prog="eigenhelm-harness",
85
+ description="Compare two corpora with statistical testing",
86
+ )
87
+ parser.add_argument("--before", required=True, type=Path, help="Before corpus directory")
88
+ parser.add_argument("--after", required=True, type=Path, help="After corpus directory")
89
+ parser.add_argument("--model", default=None, help="Path to .npz eigenspace model")
90
+ parser.add_argument("--json", dest="json_output", action="store_true", help="JSON output")
91
+ args = parser.parse_args(argv)
92
+
93
+ try:
94
+ eigenspace = None
95
+ if args.model:
96
+ from eigenhelm.eigenspace import load_model
97
+
98
+ eigenspace = load_model(args.model)
99
+
100
+ from eigenhelm.harness.runner import run_harness
101
+
102
+ report = run_harness(args.before, args.after, eigenspace=eigenspace)
103
+
104
+ if args.json_output:
105
+ print(format_harness_json(report))
106
+ else:
107
+ print(format_harness_human(report))
108
+
109
+ return 0
110
+
111
+ except ValueError as exc:
112
+ print(f"ERROR: {exc}", file=sys.stderr)
113
+ return 1
114
+ except Exception as exc:
115
+ print(f"ERROR: {exc}", file=sys.stderr)
116
+ return 2
117
+
118
+
119
+ if __name__ == "__main__":
120
+ sys.exit(main())
@@ -0,0 +1,86 @@
1
+ """eigenhelm-inspect: Inspect a trained .npz eigenspace model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ def main(argv: list[str] | None = None) -> None:
12
+ """Entry point for eigenhelm-inspect CLI.
13
+
14
+ Exit codes:
15
+ 0 — success
16
+ 1 — invalid model file (missing keys, bad format)
17
+ """
18
+ parser = argparse.ArgumentParser(
19
+ prog="eigenhelm-inspect",
20
+ description="Inspect a trained .npz eigenspace model.",
21
+ )
22
+ parser.add_argument(
23
+ "model_path",
24
+ metavar="model-path",
25
+ type=Path,
26
+ help="Path to a .npz eigenspace model",
27
+ )
28
+ parser.add_argument(
29
+ "--json",
30
+ action="store_true",
31
+ dest="as_json",
32
+ help="Output as JSON instead of plain text",
33
+ )
34
+
35
+ args = parser.parse_args(argv)
36
+
37
+ from eigenhelm.training import inspect_model
38
+
39
+ try:
40
+ info = inspect_model(args.model_path)
41
+ except FileNotFoundError as exc:
42
+ print(f"eigenhelm-inspect: error: {exc}", file=sys.stderr)
43
+ sys.exit(1)
44
+ except KeyError as exc:
45
+ print(f"eigenhelm-inspect: invalid model file: {exc}", file=sys.stderr)
46
+ sys.exit(1)
47
+ except Exception as exc:
48
+ print(f"eigenhelm-inspect: error: {exc}", file=sys.stderr)
49
+ sys.exit(1)
50
+
51
+ if args.as_json:
52
+ evr = info["explained_variance_ratio"]
53
+ out = {
54
+ "version": info["version"],
55
+ "n_components": info["n_components"],
56
+ "corpus_hash": info["corpus_hash"],
57
+ "projection_shape": list(info["projection_shape"]),
58
+ "cumulative_variance": info["cumulative_variance"],
59
+ "explained_variance_ratio": evr.tolist() if evr is not None else None,
60
+ "mean_range": list(info["mean_range"]),
61
+ "std_range": list(info["std_range"]),
62
+ }
63
+ print(json.dumps(out, indent=2))
64
+ else:
65
+ evr = info["explained_variance_ratio"]
66
+ if evr is not None:
67
+ per_pc = " ".join(f"PC{i + 1}: {v * 100:.1f}%" for i, v in enumerate(evr))
68
+ variance_line = f"{info['cumulative_variance'] * 100:.1f}% cumulative"
69
+ else:
70
+ per_pc = "(not available)"
71
+ variance_line = "N/A"
72
+
73
+ lines = [
74
+ "eigenhelm-inspect: Model summary",
75
+ f" Version: {info['version']}",
76
+ f" Components: {info['n_components']}",
77
+ f" Corpus hash: {info['corpus_hash']}",
78
+ f" Projection: {info['projection_shape']}",
79
+ f" Variance: {variance_line}",
80
+ f" {per_pc}",
81
+ f" Feature mean: [{info['mean_range'][0]:.2f}, {info['mean_range'][1]:.2f}] (range)",
82
+ f" Feature std: [{info['std_range'][0]:.2f}, {info['std_range'][1]:.2f}] (range)",
83
+ ]
84
+ print("\n".join(lines))
85
+
86
+ sys.exit(0)
eigenhelm/cli/serve.py ADDED
@@ -0,0 +1,76 @@
1
+ """eigenhelm-serve CLI — start the FastAPI sidecar server.
2
+
3
+ Usage:
4
+ eigenhelm-serve --host 0.0.0.0 --port 8080 --model model.npz
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import sys
11
+
12
+
13
+ def main(argv: list[str] | None = None) -> None:
14
+ """Entry point for eigenhelm-serve."""
15
+ try:
16
+ import uvicorn # noqa: F401, I001
17
+ from eigenhelm.serve import create_app # noqa: I001
18
+ except ImportError:
19
+ print(
20
+ "ERROR: eigenhelm[serve] extras not installed. Run: pip install 'eigenhelm[serve]'",
21
+ file=sys.stderr,
22
+ )
23
+ sys.exit(1)
24
+
25
+ parser = argparse.ArgumentParser(
26
+ prog="eigenhelm-serve",
27
+ description="Start the eigenhelm evaluation sidecar server",
28
+ )
29
+ parser.add_argument("--host", default="0.0.0.0", help="Bind address (default: 0.0.0.0)")
30
+ parser.add_argument("--port", type=int, default=8080, help="Port (default: 8080)")
31
+ parser.add_argument("--model", default=None, help="Path to .npz eigenspace model")
32
+ parser.add_argument(
33
+ "--timeout-graceful-shutdown",
34
+ type=int,
35
+ default=30,
36
+ help="Seconds to wait for in-flight requests on SIGTERM (default: 30)",
37
+ )
38
+ args = parser.parse_args(argv)
39
+
40
+ eigenspace = None
41
+ if args.model:
42
+ try:
43
+ from eigenhelm.eigenspace import load_model
44
+
45
+ eigenspace = load_model(args.model)
46
+ print(
47
+ f"INFO: Loading eigenspace model from {args.model} "
48
+ f"(version={eigenspace.version}, corpus_hash={eigenspace.corpus_hash})",
49
+ file=sys.stderr,
50
+ )
51
+ except (FileNotFoundError, OSError) as exc:
52
+ print(f"ERROR: Failed to load model: {exc}", file=sys.stderr)
53
+ sys.exit(1)
54
+ else:
55
+ print(
56
+ "WARNING: No eigenspace model loaded; running in low-confidence mode",
57
+ file=sys.stderr,
58
+ )
59
+
60
+ app = create_app(eigenspace=eigenspace)
61
+ model_status = "loaded" if eigenspace else "none"
62
+ print(
63
+ f"INFO: eigenhelm-serve starting on {args.host}:{args.port} (model={model_status})",
64
+ file=sys.stderr,
65
+ )
66
+
67
+ uvicorn.run(
68
+ app,
69
+ host=args.host,
70
+ port=args.port,
71
+ timeout_graceful_shutdown=args.timeout_graceful_shutdown,
72
+ )
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()