zeroquantz 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 (105) hide show
  1. zeroquantz/__init__.py +14 -0
  2. zeroquantz/__main__.py +8 -0
  3. zeroquantz/agent/__init__.py +16 -0
  4. zeroquantz/agent/dispatcher.py +520 -0
  5. zeroquantz/agent/intents.py +46 -0
  6. zeroquantz/agent/parser.py +255 -0
  7. zeroquantz/benchmark/__init__.py +7 -0
  8. zeroquantz/benchmark/latency.py +66 -0
  9. zeroquantz/benchmark/memory.py +41 -0
  10. zeroquantz/benchmark/quality.py +38 -0
  11. zeroquantz/benchmark/runner.py +151 -0
  12. zeroquantz/cli/__init__.py +7 -0
  13. zeroquantz/cli/app.py +98 -0
  14. zeroquantz/cli/commands.py +459 -0
  15. zeroquantz/cli/interactive.py +56 -0
  16. zeroquantz/core/__init__.py +7 -0
  17. zeroquantz/core/artifacts.py +179 -0
  18. zeroquantz/core/context.py +127 -0
  19. zeroquantz/core/events.py +30 -0
  20. zeroquantz/core/exceptions.py +105 -0
  21. zeroquantz/core/session.py +202 -0
  22. zeroquantz/core/subenv.py +202 -0
  23. zeroquantz/deploy/__init__.py +25 -0
  24. zeroquantz/deploy/assets.py +161 -0
  25. zeroquantz/deploy/launcher.py +80 -0
  26. zeroquantz/deploy/runtime_env.py +66 -0
  27. zeroquantz/deploy/targets.py +154 -0
  28. zeroquantz/export/__init__.py +8 -0
  29. zeroquantz/export/exporter.py +68 -0
  30. zeroquantz/export/report.py +203 -0
  31. zeroquantz/hardware/__init__.py +15 -0
  32. zeroquantz/hardware/capabilities.py +152 -0
  33. zeroquantz/hardware/detector.py +200 -0
  34. zeroquantz/hardware/gpu.py +31 -0
  35. zeroquantz/models/__init__.py +8 -0
  36. zeroquantz/models/architecture.py +168 -0
  37. zeroquantz/models/downloader.py +161 -0
  38. zeroquantz/models/hf_auth.py +105 -0
  39. zeroquantz/models/inspector.py +249 -0
  40. zeroquantz/models/metadata.py +108 -0
  41. zeroquantz/models/search.py +71 -0
  42. zeroquantz/optimization/__init__.py +22 -0
  43. zeroquantz/optimization/candidate.py +272 -0
  44. zeroquantz/optimization/constraints.py +70 -0
  45. zeroquantz/optimization/fit.py +203 -0
  46. zeroquantz/optimization/pareto.py +66 -0
  47. zeroquantz/optimization/planner.py +297 -0
  48. zeroquantz/optimization/recommender.py +149 -0
  49. zeroquantz/profiling/__init__.py +18 -0
  50. zeroquantz/profiling/calibration.py +74 -0
  51. zeroquantz/profiling/sensitivity.py +234 -0
  52. zeroquantz/quantization/__init__.py +17 -0
  53. zeroquantz/quantization/backends/__init__.py +8 -0
  54. zeroquantz/quantization/backends/bitsandbytes.py +210 -0
  55. zeroquantz/quantization/backends/torchao.py +198 -0
  56. zeroquantz/quantization/base.py +136 -0
  57. zeroquantz/quantization/catalog.py +321 -0
  58. zeroquantz/quantization/config.py +106 -0
  59. zeroquantz/quantization/gguf_pipeline.py +210 -0
  60. zeroquantz/quantization/isolated.py +248 -0
  61. zeroquantz/quantization/memory.py +133 -0
  62. zeroquantz/quantization/native.py +91 -0
  63. zeroquantz/quantization/registry.py +101 -0
  64. zeroquantz/render.py +341 -0
  65. zeroquantz/runtimes/__init__.py +18 -0
  66. zeroquantz/runtimes/base.py +64 -0
  67. zeroquantz/runtimes/compatibility.py +91 -0
  68. zeroquantz/runtimes/registry.py +70 -0
  69. zeroquantz/runtimes/transformers.py +53 -0
  70. zeroquantz/runtimes/vllm.py +83 -0
  71. zeroquantz/tui/__init__.py +13 -0
  72. zeroquantz/tui/app.py +77 -0
  73. zeroquantz/tui/banner.py +47 -0
  74. zeroquantz/tui/screens/__init__.py +25 -0
  75. zeroquantz/tui/screens/confirm.py +41 -0
  76. zeroquantz/tui/screens/execute.py +194 -0
  77. zeroquantz/tui/screens/model_select.py +206 -0
  78. zeroquantz/tui/screens/plan.py +177 -0
  79. zeroquantz/tui/screens/quantize_select.py +272 -0
  80. zeroquantz/tui/screens/settings.py +219 -0
  81. zeroquantz/tui/screens/token.py +94 -0
  82. zeroquantz/tui/screens/welcome.py +128 -0
  83. zeroquantz/tui/screens/workspace.py +175 -0
  84. zeroquantz/tui/styles/app.tcss +424 -0
  85. zeroquantz/tui/widgets/__init__.py +9 -0
  86. zeroquantz/tui/widgets/chip.py +36 -0
  87. zeroquantz/tui/widgets/sidebar.py +107 -0
  88. zeroquantz/tui/widgets/status_bar.py +43 -0
  89. zeroquantz/utils/__init__.py +8 -0
  90. zeroquantz/utils/config.py +46 -0
  91. zeroquantz/utils/env.py +78 -0
  92. zeroquantz/utils/logging.py +73 -0
  93. zeroquantz/utils/metrics.py +98 -0
  94. zeroquantz/utils/paths.py +57 -0
  95. zeroquantz/utils/units.py +134 -0
  96. zeroquantz/verification/__init__.py +17 -0
  97. zeroquantz/verification/logits.py +55 -0
  98. zeroquantz/verification/report.py +186 -0
  99. zeroquantz/verification/weights.py +44 -0
  100. zeroquantz/version.py +8 -0
  101. zeroquantz-0.1.0.dist-info/METADATA +72 -0
  102. zeroquantz-0.1.0.dist-info/RECORD +105 -0
  103. zeroquantz-0.1.0.dist-info/WHEEL +4 -0
  104. zeroquantz-0.1.0.dist-info/entry_points.txt +2 -0
  105. zeroquantz-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,424 @@
1
+ /* ZeroQuantz TUI theme — OpenCode-style dark, cyan/green accents. */
2
+
3
+ Screen {
4
+ background: #0b0e14;
5
+ color: #c9d1d9;
6
+ }
7
+
8
+ /* ---- welcome screen ---- */
9
+
10
+ #welcome {
11
+ align: center middle;
12
+ height: 1fr;
13
+ }
14
+
15
+ .banner {
16
+ color: #c9d1d9;
17
+ text-style: bold;
18
+ content-align: center middle;
19
+ width: auto;
20
+ margin-bottom: 1;
21
+ }
22
+
23
+ .tagline {
24
+ color: #6e7681;
25
+ content-align: center middle;
26
+ width: auto;
27
+ margin-bottom: 1;
28
+ }
29
+
30
+ .status-line {
31
+ content-align: center middle;
32
+ width: auto;
33
+ margin-bottom: 1;
34
+ }
35
+
36
+ .prompt-line {
37
+ color: #58a6ff;
38
+ text-style: bold;
39
+ content-align: center middle;
40
+ width: auto;
41
+ margin-bottom: 1;
42
+ }
43
+
44
+ #menu {
45
+ width: auto;
46
+ max-width: 72;
47
+ height: auto;
48
+ background: transparent;
49
+ border: none;
50
+ padding: 0 2;
51
+ }
52
+
53
+ OptionList {
54
+ background: transparent;
55
+ border: none;
56
+ }
57
+
58
+ OptionList > .option-list--option-highlighted {
59
+ background: #172033;
60
+ text-style: bold;
61
+ }
62
+
63
+ OptionList:focus > .option-list--option-highlighted {
64
+ background: #1f2b40;
65
+ text-style: bold;
66
+ }
67
+
68
+ /* ---- workspace screen ---- */
69
+
70
+ #topbar {
71
+ height: 1;
72
+ background: #11151c;
73
+ color: #8b949e;
74
+ padding: 0 1;
75
+ }
76
+
77
+ #body {
78
+ height: 1fr;
79
+ }
80
+
81
+ #conversation {
82
+ width: 1fr;
83
+ height: 1fr;
84
+ background: #0d1117;
85
+ padding: 0 1;
86
+ scrollbar-size-vertical: 1;
87
+ }
88
+
89
+ #sidebar {
90
+ width: 38;
91
+ height: 1fr;
92
+ background: #0d1117;
93
+ border-left: solid #21262d;
94
+ padding: 0 1;
95
+ display: block;
96
+ }
97
+
98
+ #sidebar.hidden {
99
+ display: none;
100
+ }
101
+
102
+ #progress {
103
+ height: auto;
104
+ color: #d29922;
105
+ padding: 0 1;
106
+ display: none;
107
+ }
108
+
109
+ #progress.active {
110
+ display: block;
111
+ }
112
+
113
+ #prompt {
114
+ dock: bottom;
115
+ border: tall #21262d;
116
+ background: #0d1117;
117
+ color: #c9d1d9;
118
+ }
119
+
120
+ #prompt:focus {
121
+ border: tall #58a6ff;
122
+ }
123
+
124
+ /* ---- wizard pages ---- */
125
+
126
+ .page-title {
127
+ color: #58a6ff;
128
+ text-style: bold;
129
+ padding: 1 2 0 2;
130
+ }
131
+
132
+ .page-subtitle {
133
+ color: #6e7681;
134
+ padding: 0 2 1 2;
135
+ }
136
+
137
+ .page-hint {
138
+ color: #6e7681;
139
+ padding: 1 2;
140
+ }
141
+
142
+ #model-page {
143
+ padding: 1 2;
144
+ height: 1fr;
145
+ }
146
+
147
+ #model-input {
148
+ border: tall #21262d;
149
+ background: #0d1117;
150
+ margin: 0 0 1 0;
151
+ }
152
+
153
+ #model-input:focus {
154
+ border: tall #58a6ff;
155
+ }
156
+
157
+ #model-suggestions {
158
+ height: 1fr;
159
+ background: transparent;
160
+ border: none;
161
+ margin-bottom: 1;
162
+ }
163
+
164
+ #model-summary {
165
+ height: auto;
166
+ max-height: 12;
167
+ padding: 1 0;
168
+ }
169
+
170
+ #model-actions {
171
+ height: auto;
172
+ align: left middle;
173
+ }
174
+
175
+ #download-page {
176
+ padding: 1 2;
177
+ height: 1fr;
178
+ }
179
+
180
+ #dl-model {
181
+ height: auto;
182
+ margin-bottom: 1;
183
+ }
184
+
185
+ #dl-progress {
186
+ width: 100%;
187
+ margin: 1 0;
188
+ }
189
+
190
+ #quant-page {
191
+ padding: 1 2;
192
+ height: 1fr;
193
+ }
194
+
195
+ #quant-model {
196
+ padding: 0 0 1 0;
197
+ height: auto;
198
+ }
199
+
200
+ .ctl-row {
201
+ height: 1;
202
+ width: 1fr;
203
+ }
204
+
205
+ .ctl-label {
206
+ color: #8b949e;
207
+ width: auto;
208
+ padding: 0 1 0 2;
209
+ }
210
+
211
+ .ctl-value {
212
+ color: #3fb950;
213
+ text-style: bold;
214
+ width: auto;
215
+ padding: 0 1;
216
+ }
217
+
218
+ .chip {
219
+ width: auto;
220
+ height: 1;
221
+ padding: 0 1;
222
+ margin: 0 1 0 0;
223
+ color: #c9d1d9;
224
+ }
225
+
226
+ .chip:hover {
227
+ background: #161b22;
228
+ }
229
+
230
+ .chip:focus {
231
+ background: #161b22;
232
+ text-style: bold;
233
+ }
234
+
235
+ .chip.-selected {
236
+ background: #1f2b40;
237
+ color: #58a6ff;
238
+ text-style: bold;
239
+ }
240
+
241
+ #quant-table {
242
+ height: 1fr;
243
+ margin-top: 1;
244
+ background: #0d1117;
245
+ }
246
+
247
+ /* ---- plan + execute ---- */
248
+
249
+ #plan-page,
250
+ #exec-page {
251
+ padding: 1 2;
252
+ height: 1fr;
253
+ }
254
+
255
+ #deploy-chips {
256
+ height: 1;
257
+ }
258
+
259
+ #plan-scroll {
260
+ height: 1fr;
261
+ }
262
+
263
+ #plan-body {
264
+ height: auto;
265
+ }
266
+
267
+ #plan-status {
268
+ height: auto;
269
+ }
270
+
271
+ #exec-stages {
272
+ height: auto;
273
+ padding: 0 0 1 0;
274
+ }
275
+
276
+ #exec-progress {
277
+ width: 100%;
278
+ margin: 0 0 1 0;
279
+ }
280
+
281
+ #exec-log {
282
+ height: 1fr;
283
+ min-height: 6;
284
+ background: #0d1117;
285
+ border: round #21262d;
286
+ padding: 0 1;
287
+ }
288
+
289
+ DataTable > .datatable--cursor {
290
+ background: #1f2b40;
291
+ text-style: bold;
292
+ }
293
+
294
+ DataTable > .datatable--header {
295
+ color: #58a6ff;
296
+ text-style: bold;
297
+ background: #11151c;
298
+ }
299
+
300
+ #summary-page {
301
+ padding: 1 2;
302
+ height: 1fr;
303
+ }
304
+
305
+ #summary-scroll {
306
+ height: 1fr;
307
+ }
308
+
309
+ #summary-body {
310
+ height: auto;
311
+ }
312
+
313
+ #summary-log {
314
+ height: 9;
315
+ min-height: 3;
316
+ background: #0d1117;
317
+ border: round #21262d;
318
+ padding: 0 1;
319
+ margin-top: 1;
320
+ }
321
+
322
+ /* ---- settings ---- */
323
+
324
+ #settings-page {
325
+ padding: 1 2;
326
+ height: 1fr;
327
+ }
328
+
329
+ #settings-tabs {
330
+ height: 1fr;
331
+ }
332
+
333
+ #dl-table,
334
+ #q-table {
335
+ height: 1fr;
336
+ background: #0d1117;
337
+ }
338
+
339
+ #tok-status {
340
+ height: auto;
341
+ padding: 1 0;
342
+ }
343
+
344
+ /* ---- adjust page ---- */
345
+
346
+ #adjust-page {
347
+ padding: 1 2;
348
+ height: 1fr;
349
+ }
350
+
351
+ #adjust-body {
352
+ height: auto;
353
+ padding: 1 0;
354
+ }
355
+
356
+ /* ---- confirm modal ---- */
357
+
358
+ ConfirmScreen {
359
+ align: center middle;
360
+ background: #000000 60%;
361
+ }
362
+
363
+ #confirm-dialog {
364
+ width: 74;
365
+ max-width: 90%;
366
+ height: auto;
367
+ background: #0d1117;
368
+ border: round #f85149;
369
+ padding: 1 2;
370
+ }
371
+
372
+ #confirm-message {
373
+ height: auto;
374
+ padding-bottom: 1;
375
+ }
376
+
377
+ /* ---- token modal ---- */
378
+
379
+ TokenScreen {
380
+ align: center middle;
381
+ background: #000000 60%;
382
+ }
383
+
384
+ #token-dialog {
385
+ width: 78;
386
+ max-width: 90%;
387
+ height: auto;
388
+ background: #0d1117;
389
+ border: round #58a6ff;
390
+ padding: 1 2;
391
+ }
392
+
393
+ .dialog-title {
394
+ color: #58a6ff;
395
+ text-style: bold;
396
+ }
397
+
398
+ .dialog-sub {
399
+ color: #6e7681;
400
+ padding: 0 0 1 0;
401
+ }
402
+
403
+ #token-status {
404
+ height: auto;
405
+ padding: 0 0 1 0;
406
+ }
407
+
408
+ #token-input {
409
+ border: tall #21262d;
410
+ background: #0b0e14;
411
+ }
412
+
413
+ #token-input:focus {
414
+ border: tall #58a6ff;
415
+ }
416
+
417
+ #token-actions {
418
+ height: auto;
419
+ padding-top: 1;
420
+ }
421
+
422
+ #token-actions Button {
423
+ margin: 0 2 0 0;
424
+ }
@@ -0,0 +1,9 @@
1
+ """ZeroQuantz TUI widgets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.tui.widgets.chip import Chip
6
+ from zeroquantz.tui.widgets.sidebar import Sidebar
7
+ from zeroquantz.tui.widgets.status_bar import StatusBar
8
+
9
+ __all__ = ["Chip", "Sidebar", "StatusBar"]
@@ -0,0 +1,36 @@
1
+ """A small, clickable (and keyboard-focusable) chip for inline segmented controls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.binding import Binding
6
+ from textual.message import Message
7
+ from textual.widgets import Static
8
+
9
+
10
+ class Chip(Static):
11
+ """A selectable inline control. Click it, or focus it and press Enter/Space."""
12
+
13
+ can_focus = True
14
+ BINDINGS = [Binding("enter,space", "select", "Select", show=False)]
15
+
16
+ class Selected(Message):
17
+ def __init__(self, group: str, value: str) -> None:
18
+ self.group = group
19
+ self.value = value
20
+ super().__init__()
21
+
22
+ def __init__(self, label: str, *, group: str, value: str, selected: bool = False) -> None:
23
+ super().__init__(label)
24
+ self.chip_group = group
25
+ self.chip_value = value
26
+ self.add_class("chip")
27
+ self.set_class(selected, "-selected")
28
+
29
+ def set_selected(self, selected: bool) -> None:
30
+ self.set_class(selected, "-selected")
31
+
32
+ def on_click(self) -> None:
33
+ self.post_message(self.Selected(self.chip_group, self.chip_value))
34
+
35
+ def action_select(self) -> None:
36
+ self.post_message(self.Selected(self.chip_group, self.chip_value))
@@ -0,0 +1,107 @@
1
+ """Collapsible side panel: Model / Hardware / Goal / Plan / Session at a glance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from rich.console import Group
8
+ from rich.rule import Rule
9
+ from rich.table import Table
10
+ from rich.text import Text
11
+ from textual.widgets import Static
12
+
13
+ from zeroquantz.utils import units
14
+
15
+ if TYPE_CHECKING:
16
+ from zeroquantz.core.context import AppContext
17
+
18
+ _HEAD = "bold cyan"
19
+ _MUTED = "grey62"
20
+
21
+
22
+ def _section(title: str, rows: list[tuple[str, str]]) -> Group:
23
+ grid = Table.grid(padding=(0, 1))
24
+ grid.add_column(style=_MUTED, no_wrap=True)
25
+ grid.add_column(style="white")
26
+ for k, v in rows:
27
+ grid.add_row(k, v)
28
+ return Group(Text(title, style=_HEAD), grid, Text(""))
29
+
30
+
31
+ class Sidebar(Static):
32
+ """Reactive summary of the current session state."""
33
+
34
+ def update_context(self, ctx: AppContext) -> None:
35
+ sections: list = []
36
+
37
+ # Model
38
+ mp = ctx.session.model_profile
39
+ if mp is not None:
40
+ sections.append(
41
+ _section(
42
+ "Model",
43
+ [
44
+ ("id", mp.model_id),
45
+ ("arch", mp.architecture or "?"),
46
+ ("params", units.humanize_params(mp.parameter_count)),
47
+ ("dtype", mp.dtype),
48
+ ("size", f"{mp.estimated_weight_size_gb:.1f} GB"),
49
+ ],
50
+ )
51
+ )
52
+ else:
53
+ sections.append(_section("Model", [("", "none loaded")]))
54
+
55
+ # Goal
56
+ goal = ctx.session.goal
57
+ goal_rows = [("objective", str(goal.objective))]
58
+ if goal.max_vram_gb:
59
+ goal_rows.append(("max VRAM", f"{goal.max_vram_gb:g} GB"))
60
+ if goal.runtime:
61
+ goal_rows.append(("runtime", goal.runtime))
62
+ if goal.max_quality_loss:
63
+ goal_rows.append(("max loss", f"{goal.max_quality_loss*100:g}%"))
64
+ sections.append(_section("Goal", goal_rows))
65
+
66
+ # Hardware
67
+ hw = ctx.hardware
68
+ sections.append(
69
+ _section(
70
+ "Hardware",
71
+ [
72
+ ("gpu", hw.gpu_name),
73
+ ("vram", f"{hw.total_vram_gb:g} GB" if hw.total_vram_gb else "n/a"),
74
+ ("sm", hw.sm or "-"),
75
+ ("cuda", hw.cuda_version or ("yes" if hw.cuda_available else "no")),
76
+ ],
77
+ )
78
+ )
79
+
80
+ # Plan
81
+ plan = ctx.session.plan
82
+ if plan is not None:
83
+ sections.append(
84
+ _section(
85
+ "Active plan",
86
+ [
87
+ ("scheme", f"{plan.low_precision}/{plan.high_precision}"),
88
+ ("size", f"{plan.estimated_size_gb:.1f} GB"),
89
+ ("vram", f"{plan.estimated_vram_gb:.1f} GB"),
90
+ ("fits", "yes" if plan.fits_budget else "no"),
91
+ ],
92
+ )
93
+ )
94
+
95
+ # Session
96
+ sections.append(
97
+ _section(
98
+ "Session",
99
+ [
100
+ ("name", ctx.session.name),
101
+ ("ops", str(len(ctx.session.operation_log))),
102
+ ("selected", ctx.session.selected_method or "-"),
103
+ ],
104
+ )
105
+ )
106
+
107
+ self.update(Group(Rule(style="grey30"), *sections))
@@ -0,0 +1,43 @@
1
+ """Top status bar: model + goal on the left, GPU + VRAM on the right."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from rich.table import Table
8
+ from rich.text import Text
9
+ from textual.widgets import Static
10
+
11
+ if TYPE_CHECKING:
12
+ from zeroquantz.core.context import AppContext
13
+
14
+
15
+ class StatusBar(Static):
16
+ """A one-line context header, updated as the session changes."""
17
+
18
+ def update_context(self, ctx: AppContext) -> None:
19
+ session = ctx.session
20
+ model = session.model_id or "no model"
21
+ goal = session.goal
22
+ goal_bits = [str(goal.objective)]
23
+ if goal.max_vram_gb:
24
+ goal_bits.append(f"≤{goal.max_vram_gb:g}GB")
25
+ if goal.runtime:
26
+ goal_bits.append(goal.runtime)
27
+ left = Text.assemble(
28
+ ("Model ", "grey62"),
29
+ (model, "bold #58a6ff"),
30
+ (" Goal ", "grey62"),
31
+ (" · ".join(goal_bits), "white"),
32
+ )
33
+ vram = f"{ctx.hardware.total_vram_gb:g} GB" if ctx.hardware.total_vram_gb else "n/a"
34
+ right = Text.assemble(
35
+ (ctx.hardware.gpu_name, "white"),
36
+ (" VRAM ", "grey62"),
37
+ (vram, "bold #3fb950" if ctx.hardware.cuda_available else "bold #d29922"),
38
+ )
39
+ grid = Table.grid(expand=True)
40
+ grid.add_column(justify="left", ratio=1)
41
+ grid.add_column(justify="right")
42
+ grid.add_row(left, right)
43
+ self.update(grid)
@@ -0,0 +1,8 @@
1
+ """Small, dependency-light helpers shared across ZeroQuantz."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from zeroquantz.utils import units
6
+ from zeroquantz.utils.paths import ZeroQuantzPaths, paths
7
+
8
+ __all__ = ["ZeroQuantzPaths", "paths", "units"]
@@ -0,0 +1,46 @@
1
+ """Load ``~/.zeroquantz/config.toml`` into a typed :class:`AppConfig`.
2
+
3
+ Precedence (low -> high) is defaults < config file < session < CLI flags. This
4
+ module only handles the file layer; callers apply the higher-precedence layers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import tomllib
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from pydantic import BaseModel
14
+
15
+ from zeroquantz.core.exceptions import ConfigError
16
+ from zeroquantz.utils.paths import paths
17
+
18
+
19
+ class BenchmarkConfig(BaseModel):
20
+ warmup_runs: int = 3
21
+ runs: int = 10
22
+
23
+
24
+ class AppConfig(BaseModel):
25
+ default_runtime: str | None = None
26
+ objective: str = "balanced"
27
+ cache_dir: str | None = None
28
+ benchmark: BenchmarkConfig = BenchmarkConfig()
29
+
30
+
31
+ def load_config(path: str | Path | None = None) -> AppConfig:
32
+ config_path = Path(path) if path is not None else paths().config_file
33
+ if not config_path.exists():
34
+ return AppConfig()
35
+ try:
36
+ data: dict[str, Any] = tomllib.loads(config_path.read_text(encoding="utf-8"))
37
+ except (OSError, tomllib.TOMLDecodeError) as exc:
38
+ raise ConfigError(
39
+ f"Could not read config at {config_path}.", detail=str(exc)
40
+ ) from exc
41
+ try:
42
+ return AppConfig.model_validate(data)
43
+ except ValueError as exc:
44
+ raise ConfigError(
45
+ f"Invalid configuration in {config_path}.", detail=str(exc)
46
+ ) from exc