themis-eval 0.1.0__py3-none-any.whl → 0.2.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 (158) hide show
  1. themis/__init__.py +12 -1
  2. themis/_version.py +2 -2
  3. themis/api.py +343 -0
  4. themis/backends/__init__.py +17 -0
  5. themis/backends/execution.py +197 -0
  6. themis/backends/storage.py +260 -0
  7. themis/cli/__init__.py +5 -0
  8. themis/cli/__main__.py +6 -0
  9. themis/cli/commands/__init__.py +19 -0
  10. themis/cli/commands/benchmarks.py +221 -0
  11. themis/cli/commands/comparison.py +394 -0
  12. themis/cli/commands/config_commands.py +244 -0
  13. themis/cli/commands/cost.py +214 -0
  14. themis/cli/commands/demo.py +68 -0
  15. themis/cli/commands/info.py +90 -0
  16. themis/cli/commands/leaderboard.py +362 -0
  17. themis/cli/commands/math_benchmarks.py +318 -0
  18. themis/cli/commands/mcq_benchmarks.py +207 -0
  19. themis/cli/commands/results.py +252 -0
  20. themis/cli/commands/sample_run.py +244 -0
  21. themis/cli/commands/visualize.py +299 -0
  22. themis/cli/main.py +463 -0
  23. themis/cli/new_project.py +33 -0
  24. themis/cli/utils.py +51 -0
  25. themis/comparison/__init__.py +25 -0
  26. themis/comparison/engine.py +348 -0
  27. themis/comparison/reports.py +283 -0
  28. themis/comparison/statistics.py +402 -0
  29. themis/config/__init__.py +19 -0
  30. themis/config/loader.py +27 -0
  31. themis/config/registry.py +34 -0
  32. themis/config/runtime.py +214 -0
  33. themis/config/schema.py +112 -0
  34. themis/core/__init__.py +5 -0
  35. themis/core/conversation.py +354 -0
  36. themis/core/entities.py +184 -0
  37. themis/core/serialization.py +231 -0
  38. themis/core/tools.py +393 -0
  39. themis/core/types.py +141 -0
  40. themis/datasets/__init__.py +273 -0
  41. themis/datasets/base.py +264 -0
  42. themis/datasets/commonsense_qa.py +174 -0
  43. themis/datasets/competition_math.py +265 -0
  44. themis/datasets/coqa.py +133 -0
  45. themis/datasets/gpqa.py +190 -0
  46. themis/datasets/gsm8k.py +123 -0
  47. themis/datasets/gsm_symbolic.py +124 -0
  48. themis/datasets/math500.py +122 -0
  49. themis/datasets/med_qa.py +179 -0
  50. themis/datasets/medmcqa.py +169 -0
  51. themis/datasets/mmlu_pro.py +262 -0
  52. themis/datasets/piqa.py +146 -0
  53. themis/datasets/registry.py +201 -0
  54. themis/datasets/schema.py +245 -0
  55. themis/datasets/sciq.py +150 -0
  56. themis/datasets/social_i_qa.py +151 -0
  57. themis/datasets/super_gpqa.py +263 -0
  58. themis/evaluation/__init__.py +1 -0
  59. themis/evaluation/conditional.py +410 -0
  60. themis/evaluation/extractors/__init__.py +19 -0
  61. themis/evaluation/extractors/error_taxonomy_extractor.py +80 -0
  62. themis/evaluation/extractors/exceptions.py +7 -0
  63. themis/evaluation/extractors/identity_extractor.py +29 -0
  64. themis/evaluation/extractors/json_field_extractor.py +45 -0
  65. themis/evaluation/extractors/math_verify_extractor.py +37 -0
  66. themis/evaluation/extractors/regex_extractor.py +43 -0
  67. themis/evaluation/math_verify_utils.py +87 -0
  68. themis/evaluation/metrics/__init__.py +21 -0
  69. themis/evaluation/metrics/code/__init__.py +19 -0
  70. themis/evaluation/metrics/code/codebleu.py +144 -0
  71. themis/evaluation/metrics/code/execution.py +280 -0
  72. themis/evaluation/metrics/code/pass_at_k.py +181 -0
  73. themis/evaluation/metrics/composite_metric.py +47 -0
  74. themis/evaluation/metrics/consistency_metric.py +80 -0
  75. themis/evaluation/metrics/exact_match.py +51 -0
  76. themis/evaluation/metrics/length_difference_tolerance.py +33 -0
  77. themis/evaluation/metrics/math_verify_accuracy.py +40 -0
  78. themis/evaluation/metrics/nlp/__init__.py +21 -0
  79. themis/evaluation/metrics/nlp/bertscore.py +138 -0
  80. themis/evaluation/metrics/nlp/bleu.py +129 -0
  81. themis/evaluation/metrics/nlp/meteor.py +153 -0
  82. themis/evaluation/metrics/nlp/rouge.py +136 -0
  83. themis/evaluation/metrics/pairwise_judge_metric.py +141 -0
  84. themis/evaluation/metrics/response_length.py +33 -0
  85. themis/evaluation/metrics/rubric_judge_metric.py +134 -0
  86. themis/evaluation/pipeline.py +49 -0
  87. themis/evaluation/pipelines/__init__.py +15 -0
  88. themis/evaluation/pipelines/composable_pipeline.py +357 -0
  89. themis/evaluation/pipelines/standard_pipeline.py +348 -0
  90. themis/evaluation/reports.py +293 -0
  91. themis/evaluation/statistics/__init__.py +53 -0
  92. themis/evaluation/statistics/bootstrap.py +79 -0
  93. themis/evaluation/statistics/confidence_intervals.py +121 -0
  94. themis/evaluation/statistics/distributions.py +207 -0
  95. themis/evaluation/statistics/effect_sizes.py +124 -0
  96. themis/evaluation/statistics/hypothesis_tests.py +305 -0
  97. themis/evaluation/statistics/types.py +139 -0
  98. themis/evaluation/strategies/__init__.py +13 -0
  99. themis/evaluation/strategies/attempt_aware_evaluation_strategy.py +51 -0
  100. themis/evaluation/strategies/default_evaluation_strategy.py +25 -0
  101. themis/evaluation/strategies/evaluation_strategy.py +24 -0
  102. themis/evaluation/strategies/judge_evaluation_strategy.py +64 -0
  103. themis/experiment/__init__.py +5 -0
  104. themis/experiment/builder.py +151 -0
  105. themis/experiment/cache_manager.py +134 -0
  106. themis/experiment/comparison.py +631 -0
  107. themis/experiment/cost.py +310 -0
  108. themis/experiment/definitions.py +62 -0
  109. themis/experiment/export.py +798 -0
  110. themis/experiment/export_csv.py +159 -0
  111. themis/experiment/integration_manager.py +104 -0
  112. themis/experiment/math.py +192 -0
  113. themis/experiment/mcq.py +169 -0
  114. themis/experiment/orchestrator.py +415 -0
  115. themis/experiment/pricing.py +317 -0
  116. themis/experiment/storage.py +1458 -0
  117. themis/experiment/visualization.py +588 -0
  118. themis/generation/__init__.py +1 -0
  119. themis/generation/agentic_runner.py +420 -0
  120. themis/generation/batching.py +254 -0
  121. themis/generation/clients.py +143 -0
  122. themis/generation/conversation_runner.py +236 -0
  123. themis/generation/plan.py +456 -0
  124. themis/generation/providers/litellm_provider.py +221 -0
  125. themis/generation/providers/vllm_provider.py +135 -0
  126. themis/generation/router.py +34 -0
  127. themis/generation/runner.py +207 -0
  128. themis/generation/strategies.py +98 -0
  129. themis/generation/templates.py +71 -0
  130. themis/generation/turn_strategies.py +393 -0
  131. themis/generation/types.py +9 -0
  132. themis/integrations/__init__.py +0 -0
  133. themis/integrations/huggingface.py +72 -0
  134. themis/integrations/wandb.py +77 -0
  135. themis/interfaces/__init__.py +169 -0
  136. themis/presets/__init__.py +10 -0
  137. themis/presets/benchmarks.py +354 -0
  138. themis/presets/models.py +190 -0
  139. themis/project/__init__.py +20 -0
  140. themis/project/definitions.py +98 -0
  141. themis/project/patterns.py +230 -0
  142. themis/providers/__init__.py +5 -0
  143. themis/providers/registry.py +39 -0
  144. themis/server/__init__.py +28 -0
  145. themis/server/app.py +337 -0
  146. themis/utils/api_generator.py +379 -0
  147. themis/utils/cost_tracking.py +376 -0
  148. themis/utils/dashboard.py +452 -0
  149. themis/utils/logging_utils.py +41 -0
  150. themis/utils/progress.py +58 -0
  151. themis/utils/tracing.py +320 -0
  152. themis_eval-0.2.0.dist-info/METADATA +596 -0
  153. themis_eval-0.2.0.dist-info/RECORD +157 -0
  154. {themis_eval-0.1.0.dist-info → themis_eval-0.2.0.dist-info}/WHEEL +1 -1
  155. themis_eval-0.1.0.dist-info/METADATA +0 -758
  156. themis_eval-0.1.0.dist-info/RECORD +0 -8
  157. {themis_eval-0.1.0.dist-info → themis_eval-0.2.0.dist-info}/licenses/LICENSE +0 -0
  158. {themis_eval-0.1.0.dist-info → themis_eval-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,452 @@
1
+ """Dashboard generator for experiment results, costs, and statistics.
2
+
3
+ This module provides HTML dashboard generation for visualizing experiment
4
+ results, cost breakdowns, and statistical analysis.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Dict, List
11
+
12
+ from themis.evaluation import reports as eval_reports
13
+ from themis.evaluation import statistics as eval_stats
14
+ from themis.utils import cost_tracking
15
+
16
+
17
+ def generate_html_dashboard(
18
+ evaluation_report: eval_reports.EvaluationReport,
19
+ cost_summary: cost_tracking.CostSummary | None = None,
20
+ statistical_summaries: Dict[str, eval_stats.StatisticalSummary] | None = None,
21
+ output_path: str | Path = "dashboard.html",
22
+ title: str = "Themis Experiment Dashboard",
23
+ ) -> None:
24
+ """Generate HTML dashboard with evaluation results, costs, and statistics.
25
+
26
+ Args:
27
+ evaluation_report: Evaluation report with metric results
28
+ cost_summary: Optional cost summary
29
+ statistical_summaries: Optional dictionary mapping metric names to statistical summaries
30
+ output_path: Path to output HTML file
31
+ title: Dashboard title
32
+ """
33
+ output_path = Path(output_path)
34
+ output_path.parent.mkdir(parents=True, exist_ok=True)
35
+
36
+ html_content = _generate_html(
37
+ evaluation_report,
38
+ cost_summary,
39
+ statistical_summaries,
40
+ title,
41
+ )
42
+
43
+ with open(output_path, "w") as f:
44
+ f.write(html_content)
45
+
46
+
47
+ def _generate_html(
48
+ evaluation_report: eval_reports.EvaluationReport,
49
+ cost_summary: cost_tracking.CostSummary | None,
50
+ statistical_summaries: Dict[str, eval_stats.StatisticalSummary] | None,
51
+ title: str,
52
+ ) -> str:
53
+ """Generate complete HTML dashboard content."""
54
+
55
+ # Build sections
56
+ metrics_section = _build_metrics_section(evaluation_report.metrics)
57
+
58
+ stats_section = ""
59
+ if statistical_summaries:
60
+ stats_section = _build_statistics_section(statistical_summaries)
61
+
62
+ cost_section = ""
63
+ if cost_summary:
64
+ cost_section = _build_cost_section(cost_summary)
65
+
66
+ failures_section = ""
67
+ if evaluation_report.failures:
68
+ failures_section = _build_failures_section(evaluation_report.failures)
69
+
70
+ # Compose full HTML
71
+ html = f"""<!DOCTYPE html>
72
+ <html lang="en">
73
+ <head>
74
+ <meta charset="UTF-8">
75
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
76
+ <title>{title}</title>
77
+ <style>
78
+ * {{
79
+ margin: 0;
80
+ padding: 0;
81
+ box-sizing: border-box;
82
+ }}
83
+
84
+ body {{
85
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif;
86
+ line-height: 1.6;
87
+ color: #333;
88
+ background: #f5f5f5;
89
+ padding: 20px;
90
+ }}
91
+
92
+ .container {{
93
+ max-width: 1200px;
94
+ margin: 0 auto;
95
+ background: white;
96
+ padding: 30px;
97
+ border-radius: 8px;
98
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
99
+ }}
100
+
101
+ h1 {{
102
+ color: #2c3e50;
103
+ margin-bottom: 30px;
104
+ padding-bottom: 10px;
105
+ border-bottom: 3px solid #3498db;
106
+ }}
107
+
108
+ h2 {{
109
+ color: #34495e;
110
+ margin-top: 30px;
111
+ margin-bottom: 15px;
112
+ padding-bottom: 8px;
113
+ border-bottom: 2px solid #ecf0f1;
114
+ }}
115
+
116
+ h3 {{
117
+ color: #7f8c8d;
118
+ margin-top: 20px;
119
+ margin-bottom: 10px;
120
+ }}
121
+
122
+ .metric-card {{
123
+ background: #f8f9fa;
124
+ border-left: 4px solid #3498db;
125
+ padding: 15px;
126
+ margin-bottom: 15px;
127
+ border-radius: 4px;
128
+ }}
129
+
130
+ .metric-name {{
131
+ font-size: 18px;
132
+ font-weight: 600;
133
+ color: #2c3e50;
134
+ margin-bottom: 8px;
135
+ }}
136
+
137
+ .metric-value {{
138
+ font-size: 32px;
139
+ font-weight: 700;
140
+ color: #3498db;
141
+ margin: 10px 0;
142
+ }}
143
+
144
+ .metric-details {{
145
+ display: grid;
146
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
147
+ gap: 10px;
148
+ margin-top: 10px;
149
+ }}
150
+
151
+ .detail-item {{
152
+ background: white;
153
+ padding: 10px;
154
+ border-radius: 4px;
155
+ }}
156
+
157
+ .detail-label {{
158
+ font-size: 12px;
159
+ color: #7f8c8d;
160
+ text-transform: uppercase;
161
+ letter-spacing: 0.5px;
162
+ }}
163
+
164
+ .detail-value {{
165
+ font-size: 16px;
166
+ font-weight: 600;
167
+ color: #2c3e50;
168
+ margin-top: 4px;
169
+ }}
170
+
171
+ .cost-summary {{
172
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
173
+ color: white;
174
+ padding: 20px;
175
+ border-radius: 8px;
176
+ margin-bottom: 20px;
177
+ }}
178
+
179
+ .cost-total {{
180
+ font-size: 48px;
181
+ font-weight: 700;
182
+ margin: 10px 0;
183
+ }}
184
+
185
+ .cost-breakdown {{
186
+ display: grid;
187
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
188
+ gap: 15px;
189
+ margin-top: 15px;
190
+ }}
191
+
192
+ .cost-item {{
193
+ background: rgba(255, 255, 255, 0.1);
194
+ padding: 12px;
195
+ border-radius: 4px;
196
+ }}
197
+
198
+ .cost-item-name {{
199
+ font-size: 14px;
200
+ opacity: 0.9;
201
+ }}
202
+
203
+ .cost-item-value {{
204
+ font-size: 20px;
205
+ font-weight: 600;
206
+ margin-top: 4px;
207
+ }}
208
+
209
+ .failures {{
210
+ background: #fee;
211
+ border-left: 4px solid #e74c3c;
212
+ padding: 15px;
213
+ border-radius: 4px;
214
+ }}
215
+
216
+ .failure-item {{
217
+ margin: 10px 0;
218
+ padding: 10px;
219
+ background: white;
220
+ border-radius: 4px;
221
+ }}
222
+
223
+ .confidence-interval {{
224
+ font-size: 14px;
225
+ color: #7f8c8d;
226
+ font-family: 'Courier New', monospace;
227
+ }}
228
+
229
+ .badge {{
230
+ display: inline-block;
231
+ padding: 4px 8px;
232
+ border-radius: 4px;
233
+ font-size: 12px;
234
+ font-weight: 600;
235
+ }}
236
+
237
+ .badge-success {{
238
+ background: #d4edda;
239
+ color: #155724;
240
+ }}
241
+
242
+ .badge-info {{
243
+ background: #d1ecf1;
244
+ color: #0c5460;
245
+ }}
246
+ </style>
247
+ </head>
248
+ <body>
249
+ <div class="container">
250
+ <h1>{title}</h1>
251
+
252
+ {metrics_section}
253
+
254
+ {stats_section}
255
+
256
+ {cost_section}
257
+
258
+ {failures_section}
259
+ </div>
260
+ </body>
261
+ </html>"""
262
+
263
+ return html
264
+
265
+
266
+ def _build_metrics_section(metrics: Dict[str, eval_reports.MetricAggregate]) -> str:
267
+ """Build metrics overview section."""
268
+ if not metrics:
269
+ return "<p>No metrics available.</p>"
270
+
271
+ cards = []
272
+ for metric_name, aggregate in metrics.items():
273
+ card = f"""
274
+ <div class="metric-card">
275
+ <div class="metric-name">{metric_name}</div>
276
+ <div class="metric-value">{aggregate.mean:.4f}</div>
277
+ <div class="metric-details">
278
+ <div class="detail-item">
279
+ <div class="detail-label">Samples</div>
280
+ <div class="detail-value">{aggregate.count}</div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ """
285
+ cards.append(card)
286
+
287
+ return f"""
288
+ <h2>📊 Metrics Overview</h2>
289
+ {"".join(cards)}
290
+ """
291
+
292
+
293
+ def _build_statistics_section(
294
+ statistical_summaries: Dict[str, eval_stats.StatisticalSummary],
295
+ ) -> str:
296
+ """Build detailed statistics section."""
297
+ if not statistical_summaries:
298
+ return ""
299
+
300
+ cards = []
301
+ for metric_name, summary in statistical_summaries.items():
302
+ ci_text = ""
303
+ if summary.confidence_interval_95:
304
+ ci = summary.confidence_interval_95
305
+ ci_text = f"""
306
+ <div class="detail-item">
307
+ <div class="detail-label">95% CI</div>
308
+ <div class="detail-value confidence-interval">
309
+ [{ci.lower:.4f}, {ci.upper:.4f}]
310
+ </div>
311
+ </div>
312
+ """
313
+
314
+ card = f"""
315
+ <div class="metric-card">
316
+ <div class="metric-name">{metric_name} - Statistical Analysis</div>
317
+ <div class="metric-details">
318
+ <div class="detail-item">
319
+ <div class="detail-label">Mean</div>
320
+ <div class="detail-value">{summary.mean:.4f}</div>
321
+ </div>
322
+ <div class="detail-item">
323
+ <div class="detail-label">Std Dev</div>
324
+ <div class="detail-value">{summary.std:.4f}</div>
325
+ </div>
326
+ <div class="detail-item">
327
+ <div class="detail-label">Median</div>
328
+ <div class="detail-value">{summary.median:.4f}</div>
329
+ </div>
330
+ <div class="detail-item">
331
+ <div class="detail-label">Min</div>
332
+ <div class="detail-value">{summary.min_value:.4f}</div>
333
+ </div>
334
+ <div class="detail-item">
335
+ <div class="detail-label">Max</div>
336
+ <div class="detail-value">{summary.max_value:.4f}</div>
337
+ </div>
338
+ {ci_text}
339
+ </div>
340
+ </div>
341
+ """
342
+ cards.append(card)
343
+
344
+ return f"""
345
+ <h2>📈 Statistical Analysis</h2>
346
+ {"".join(cards)}
347
+ """
348
+
349
+
350
+ def _build_cost_section(cost_summary: cost_tracking.CostSummary) -> str:
351
+ """Build cost tracking section."""
352
+
353
+ # Model breakdown
354
+ model_items = []
355
+ for model, cost in sorted(
356
+ cost_summary.cost_by_model.items(),
357
+ key=lambda x: x[1],
358
+ reverse=True,
359
+ ):
360
+ pct = (
361
+ (cost / cost_summary.total_cost * 100) if cost_summary.total_cost > 0 else 0
362
+ )
363
+ model_items.append(f"""
364
+ <div class="cost-item">
365
+ <div class="cost-item-name">{model}</div>
366
+ <div class="cost-item-value">${cost:.4f} ({pct:.1f}%)</div>
367
+ </div>
368
+ """)
369
+
370
+ # Provider breakdown
371
+ provider_items = []
372
+ for provider, cost in sorted(
373
+ cost_summary.cost_by_provider.items(),
374
+ key=lambda x: x[1],
375
+ reverse=True,
376
+ ):
377
+ pct = (
378
+ (cost / cost_summary.total_cost * 100) if cost_summary.total_cost > 0 else 0
379
+ )
380
+ provider_items.append(f"""
381
+ <div class="cost-item">
382
+ <div class="cost-item-name">{provider}</div>
383
+ <div class="cost-item-value">${cost:.4f} ({pct:.1f}%)</div>
384
+ </div>
385
+ """)
386
+
387
+ return f"""
388
+ <h2>💰 Cost Tracking</h2>
389
+ <div class="cost-summary">
390
+ <h3 style="color: white; margin-top: 0;">Total Cost</h3>
391
+ <div class="cost-total">${cost_summary.total_cost:.4f}</div>
392
+ <div class="cost-breakdown">
393
+ <div class="cost-item">
394
+ <div class="cost-item-name">Total Tokens</div>
395
+ <div class="cost-item-value">{cost_summary.total_tokens:,}</div>
396
+ </div>
397
+ <div class="cost-item">
398
+ <div class="cost-item-name">Input Tokens</div>
399
+ <div class="cost-item-value">{cost_summary.total_input_tokens:,}</div>
400
+ </div>
401
+ <div class="cost-item">
402
+ <div class="cost-item-name">Output Tokens</div>
403
+ <div class="cost-item-value">{cost_summary.total_output_tokens:,}</div>
404
+ </div>
405
+ <div class="cost-item">
406
+ <div class="cost-item-name">API Requests</div>
407
+ <div class="cost-item-value">{cost_summary.num_requests:,}</div>
408
+ </div>
409
+ </div>
410
+ </div>
411
+
412
+ <h3>Cost by Model</h3>
413
+ <div class="cost-breakdown">
414
+ {"".join(model_items)}
415
+ </div>
416
+
417
+ <h3>Cost by Provider</h3>
418
+ <div class="cost-breakdown">
419
+ {"".join(provider_items)}
420
+ </div>
421
+ """
422
+
423
+
424
+ def _build_failures_section(failures: List[eval_reports.EvaluationFailure]) -> str:
425
+ """Build failures section."""
426
+ if not failures:
427
+ return ""
428
+
429
+ failure_items = []
430
+ for failure in failures[:20]: # Limit to first 20 failures
431
+ sample_id = failure.sample_id or "Unknown"
432
+ failure_items.append(f"""
433
+ <div class="failure-item">
434
+ <strong>Sample: {sample_id}</strong><br>
435
+ {failure.message}
436
+ </div>
437
+ """)
438
+
439
+ more_text = ""
440
+ if len(failures) > 20:
441
+ more_text = f"<p><em>...and {len(failures) - 20} more failures</em></p>"
442
+
443
+ return f"""
444
+ <h2>⚠️ Failures ({len(failures)})</h2>
445
+ <div class="failures">
446
+ {"".join(failure_items)}
447
+ {more_text}
448
+ </div>
449
+ """
450
+
451
+
452
+ __all__ = ["generate_html_dashboard"]
@@ -0,0 +1,41 @@
1
+ """Utility helpers for configuring package-wide logging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Mapping
7
+
8
+ TRACE_LEVEL = 5
9
+ logging.addLevelName(TRACE_LEVEL, "TRACE")
10
+
11
+
12
+ def _trace(self, message, *args, **kwargs):
13
+ if self.isEnabledFor(TRACE_LEVEL):
14
+ self._log(TRACE_LEVEL, message, args, **kwargs)
15
+
16
+
17
+ logging.Logger.trace = _trace # type: ignore[attr-defined]
18
+
19
+ _LEVELS: Mapping[str, int] = {
20
+ "critical": logging.CRITICAL,
21
+ "error": logging.ERROR,
22
+ "warning": logging.WARNING,
23
+ "info": logging.INFO,
24
+ "debug": logging.DEBUG,
25
+ "trace": TRACE_LEVEL,
26
+ }
27
+
28
+
29
+ def configure_logging(level: str = "info") -> None:
30
+ """Configure root logging with human-friendly formatting."""
31
+
32
+ numeric_level = _LEVELS.get(level.lower(), logging.INFO)
33
+ logging.basicConfig(
34
+ level=numeric_level,
35
+ format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
36
+ datefmt="%H:%M:%S",
37
+ force=True,
38
+ )
39
+
40
+
41
+ __all__ = ["configure_logging", "TRACE_LEVEL"]
@@ -0,0 +1,58 @@
1
+ """Simple CLI-friendly progress reporter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import AbstractContextManager
6
+ from typing import Any, Callable
7
+
8
+ from tqdm import tqdm
9
+
10
+
11
+ class ProgressReporter(AbstractContextManager["ProgressReporter"]):
12
+ def __init__(
13
+ self,
14
+ *,
15
+ total: int | None,
16
+ description: str = "Processing",
17
+ unit: str = "sample",
18
+ leave: bool = False,
19
+ ) -> None:
20
+ self._total = total
21
+ self._description = description
22
+ self._unit = unit
23
+ self._leave = leave
24
+ self._pbar: tqdm | None = None
25
+
26
+ def __enter__(self) -> "ProgressReporter":
27
+ self.start()
28
+ return self
29
+
30
+ def __exit__(self, *_exc) -> None:
31
+ self.close()
32
+
33
+ def start(self) -> None:
34
+ if self._pbar is None:
35
+ self._pbar = tqdm(
36
+ total=self._total,
37
+ desc=self._description,
38
+ unit=self._unit,
39
+ leave=self._leave,
40
+ )
41
+
42
+ def close(self) -> None:
43
+ if self._pbar is not None:
44
+ self._pbar.close()
45
+ self._pbar = None
46
+
47
+ def increment(self, step: int = 1) -> None:
48
+ if self._pbar is not None:
49
+ self._pbar.update(step)
50
+
51
+ def on_result(self, _record: Any) -> None:
52
+ self.increment()
53
+
54
+ def as_callback(self) -> Callable[[Any], None]:
55
+ return self.on_result
56
+
57
+
58
+ __all__ = ["ProgressReporter"]