microlens-submit 0.12.2__py3-none-any.whl → 0.16.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.
Files changed (34) hide show
  1. microlens_submit/__init__.py +7 -157
  2. microlens_submit/cli/__init__.py +5 -0
  3. microlens_submit/cli/__main__.py +6 -0
  4. microlens_submit/cli/commands/__init__.py +1 -0
  5. microlens_submit/cli/commands/dossier.py +139 -0
  6. microlens_submit/cli/commands/export.py +177 -0
  7. microlens_submit/cli/commands/init.py +172 -0
  8. microlens_submit/cli/commands/solutions.py +722 -0
  9. microlens_submit/cli/commands/validation.py +241 -0
  10. microlens_submit/cli/main.py +120 -0
  11. microlens_submit/dossier/__init__.py +51 -0
  12. microlens_submit/dossier/dashboard.py +503 -0
  13. microlens_submit/dossier/event_page.py +370 -0
  14. microlens_submit/dossier/full_report.py +330 -0
  15. microlens_submit/dossier/solution_page.py +534 -0
  16. microlens_submit/dossier/utils.py +111 -0
  17. microlens_submit/error_messages.py +283 -0
  18. microlens_submit/models/__init__.py +28 -0
  19. microlens_submit/models/event.py +406 -0
  20. microlens_submit/models/solution.py +569 -0
  21. microlens_submit/models/submission.py +569 -0
  22. microlens_submit/tier_validation.py +208 -0
  23. microlens_submit/utils.py +373 -0
  24. microlens_submit/validate_parameters.py +478 -180
  25. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/METADATA +52 -14
  26. microlens_submit-0.16.1.dist-info/RECORD +32 -0
  27. microlens_submit/api.py +0 -1257
  28. microlens_submit/cli.py +0 -1803
  29. microlens_submit/dossier.py +0 -1443
  30. microlens_submit-0.12.2.dist-info/RECORD +0 -13
  31. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/WHEEL +0 -0
  32. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/entry_points.txt +0 -0
  33. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/licenses/LICENSE +0 -0
  34. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,534 @@
1
+ """
2
+ Solution page generation module for microlens-submit.
3
+
4
+ This module provides functionality to generate HTML pages for individual
5
+ microlensing solutions, including solution overview, parameter tables,
6
+ notes rendering, and evaluator-only sections.
7
+ """
8
+
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+
12
+ import markdown
13
+
14
+ from .. import __version__
15
+ from ..models.event import Event
16
+ from ..models.solution import Solution
17
+ from ..models.submission import Submission
18
+
19
+
20
+ def generate_solution_page(solution: Solution, event: Event, submission: Submission, output_dir: Path) -> None:
21
+ """Generate an HTML dossier page for a single solution.
22
+
23
+ Creates a detailed HTML page for a specific microlensing solution, following
24
+ the Solution_Page_Design.md specification. The page includes solution overview,
25
+ parameter tables, notes (with markdown rendering), and evaluator-only sections.
26
+
27
+ Args:
28
+ solution: The Solution object containing parameters, notes, and metadata.
29
+ event: The parent Event object for context and navigation.
30
+ submission: The grandparent Submission object for context and metadata.
31
+ output_dir: The dossier directory where the HTML file will be saved.
32
+ The file will be named {solution.solution_id}.html.
33
+
34
+ Raises:
35
+ OSError: If unable to write the HTML file or read notes file.
36
+ ValueError: If solution data is invalid.
37
+
38
+ Example:
39
+ >>> from microlens_submit import load
40
+ >>> from microlens_submit.dossier import generate_solution_page
41
+ >>> from pathlib import Path
42
+ >>>
43
+ >>> submission = load("./my_project")
44
+ >>> event = submission.get_event("EVENT001")
45
+ >>> solution = event.get_solution("solution_uuid_here")
46
+ >>>
47
+ >>> # Generate solution page
48
+ >>> generate_solution_page(solution, event, submission, Path("./dossier_output"))
49
+ >>>
50
+ >>> # Creates: ./dossier_output/solution_uuid_here.html
51
+
52
+ Note:
53
+ The solution page includes GitHub commit links if available, markdown
54
+ rendering for notes, and navigation back to the event page and dashboard.
55
+ Notes are rendered with syntax highlighting for code blocks.
56
+ """
57
+ # Prepare output directory (already created)
58
+ html = _generate_solution_page_content(solution, event, submission)
59
+ with (output_dir / f"{solution.solution_id}.html").open("w", encoding="utf-8") as f:
60
+ f.write(html)
61
+
62
+
63
+ def _generate_solution_page_content(solution: Solution, event: Event, submission: Submission) -> str:
64
+ """Generate the HTML content for a solution dossier page.
65
+
66
+ Creates the complete HTML content for a single solution page, including
67
+ parameter tables, markdown-rendered notes, plot placeholders, and
68
+ evaluator-only sections.
69
+
70
+ Args:
71
+ solution: The Solution object containing parameters, notes, and metadata.
72
+ event: The parent Event object for context and navigation.
73
+ submission: The grandparent Submission object for context and metadata.
74
+
75
+ Returns:
76
+ str: Complete HTML content as a string for the solution page.
77
+
78
+ Example:
79
+ >>> from microlens_submit import load
80
+ >>> from microlens_submit.dossier import _generate_solution_page_content
81
+ >>>
82
+ >>> submission = load("./my_project")
83
+ >>> event = submission.get_event("EVENT001")
84
+ >>> solution = event.get_solution("solution_uuid_here")
85
+ >>> html_content = _generate_solution_page_content(solution, event, submission)
86
+ >>>
87
+ >>> # Write to file
88
+ >>> with open("solution_page.html", "w", encoding="utf-8") as f:
89
+ ... f.write(html_content)
90
+
91
+ Note:
92
+ Parameter uncertainties are formatted as ±value or +upper/-lower
93
+ depending on the uncertainty format. Notes are rendered from markdown
94
+ with syntax highlighting for code blocks. GitHub commit links are
95
+ included if git information is available in compute_info.
96
+ """
97
+ # Placeholder image URLs
98
+ PARAM_COMPARISON_URL = "https://placehold.co/800x300/dfc5fa/361d49?text=Parameter+Comparison"
99
+ PARAM_DIFFERENCE_URL = "https://placehold.co/800x400/dfc5fa/361d49?text=Parameter+Difference"
100
+ PHYSICAL_PARAM_URL = "https://placehold.co/600x400/dfc5fa/361d49?text=Physical+Parameter"
101
+ CMD_WITH_SOURCE_URL = "https://placehold.co/600x400/dfc5fa/361d49?text=CMD+with+Source"
102
+ DATA_UTILIZATION_URL = "https://placehold.co/600x100/dfc5fa/361d49?text=Data+Utilization+Infographic"
103
+ # Render notes as HTML from file
104
+ notes_md = solution.get_notes(project_root=Path(submission.project_path))
105
+ notes_html = markdown.markdown(notes_md or "", extensions=["extra", "tables", "fenced_code", "nl2br"])
106
+ # Parameters table
107
+ param_rows = []
108
+ params = solution.parameters or {}
109
+ uncertainties = solution.parameter_uncertainties or {}
110
+ for k, v in params.items():
111
+ unc = uncertainties.get(k)
112
+ if unc is None:
113
+ unc_str = "N/A"
114
+ elif isinstance(unc, (list, tuple)) and len(unc) == 2:
115
+ unc_str = f"+{unc[1]}/-{unc[0]}"
116
+ else:
117
+ unc_str = f"±{unc}"
118
+ param_rows.append(
119
+ f"""
120
+ <tr class='border-b border-gray-200 hover:bg-gray-50'>
121
+ <td class='py-3 px-4'>{k}</td>
122
+ <td class='py-3 px-4'>{v}</td>
123
+ <td class='py-3 px-4'>{unc_str}</td>
124
+ </tr>
125
+ """
126
+ )
127
+ param_table = (
128
+ "\n".join(param_rows)
129
+ if param_rows
130
+ else """
131
+ <tr class='border-b border-gray-200'>
132
+ <td colspan='3' class='py-3 px-4 text-center text-gray-500'>
133
+ No parameters found
134
+ </td>
135
+ </tr>
136
+ """
137
+ )
138
+ # Higher-order effects
139
+ hoe_str = ", ".join(solution.higher_order_effects) if solution.higher_order_effects else "None"
140
+ # Plot paths (relative to solution page)
141
+ lc_plot = solution.lightcurve_plot_path or ""
142
+ lens_plot = solution.lens_plane_plot_path or ""
143
+ posterior = solution.posterior_path or ""
144
+ # Physical parameters table
145
+ phys_rows = []
146
+ phys = solution.physical_parameters or {}
147
+ for k, v in phys.items():
148
+ phys_rows.append(
149
+ f"""
150
+ <tr class='border-b border-gray-200 hover:bg-gray-50'>
151
+ <td class='py-3 px-4'>{k}</td>
152
+ <td class='py-3 px-4'>{v}</td>
153
+ </tr>
154
+ """
155
+ )
156
+ phys_table = (
157
+ "\n".join(phys_rows)
158
+ if phys_rows
159
+ else """
160
+ <tr class='border-b border-gray-200'>
161
+ <td colspan='2' class='py-3 px-4 text-center text-gray-500'>
162
+ No physical parameters found
163
+ </td>
164
+ </tr>
165
+ """
166
+ )
167
+ # GitHub commit link (if present)
168
+ repo_url = getattr(submission, "repo_url", None) or (
169
+ submission.repo_url if hasattr(submission, "repo_url") else None
170
+ )
171
+ commit = None
172
+ if solution.compute_info:
173
+ git_info = solution.compute_info.get("git_info")
174
+ if git_info:
175
+ commit = git_info.get("commit")
176
+ commit_html = ""
177
+ if repo_url and commit:
178
+ commit_short = commit[:8]
179
+ commit_url = f"{repo_url.rstrip('/')}/commit/{commit}"
180
+ commit_html = f"""<a href="{commit_url}" target="_blank" rel="noopener"
181
+ title="View this commit on GitHub"
182
+ class="inline-flex items-center space-x-1 ml-2 align-middle">
183
+ <img src="assets/github-desktop_logo.png" alt="GitHub Commit"
184
+ class="w-4 h-4 inline-block align-middle"
185
+ style="display:inline;vertical-align:middle;">
186
+ <span class="text-xs text-rtd-accent font-mono">{commit_short}</span>
187
+ </a>"""
188
+ # HTML content
189
+ html = f"""<!DOCTYPE html>
190
+ <html lang='en'>
191
+ <head>
192
+ <meta charset='UTF-8'>
193
+ <meta name='viewport' content='width=device-width, initial-scale=1.0'>
194
+ <title>Solution Dossier: {solution.alias or solution.solution_id[:8] + '...'} - {submission.team_name}</title>
195
+ <script src='https://cdn.tailwindcss.com'></script>
196
+ <script>
197
+ tailwind.config = {{
198
+ theme: {{
199
+ extend: {{
200
+ colors: {{
201
+ 'rtd-primary': '#dfc5fa',
202
+ 'rtd-secondary': '#361d49',
203
+ 'rtd-accent': '#a859e4',
204
+ 'rtd-background': '#faf7fd',
205
+ 'rtd-text': '#000',
206
+ }},
207
+ fontFamily: {{
208
+ inter: ['Inter', 'sans-serif'],
209
+ }},
210
+ }},
211
+ }},
212
+ }};
213
+ </script>
214
+ <link href='https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap' rel='stylesheet'>
215
+ <!-- Highlight.js for code syntax highlighting -->
216
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
217
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
218
+ <script>hljs.highlightAll();</script>
219
+ <style>
220
+ .prose {{
221
+ color: #000;
222
+ line-height: 1.6;
223
+ }}
224
+ .prose h1 {{
225
+ font-size: 1.5rem;
226
+ font-weight: 700;
227
+ color: #361d49;
228
+ margin-top: 1.5rem;
229
+ margin-bottom: 0.75rem;
230
+ }}
231
+ .prose h2 {{
232
+ font-size: 1.25rem;
233
+ font-weight: 600;
234
+ color: #361d49;
235
+ margin-top: 1.25rem;
236
+ margin-bottom: 0.5rem;
237
+ }}
238
+ .prose h3 {{
239
+ font-size: 1.125rem;
240
+ font-weight: 600;
241
+ color: #a859e4;
242
+ margin-top: 1rem;
243
+ margin-bottom: 0.5rem;
244
+ }}
245
+ .prose p {{
246
+ margin-bottom: 0.75rem;
247
+ }}
248
+ .prose ul, .prose ol {{
249
+ margin-left: 1.5rem;
250
+ margin-bottom: 0.75rem;
251
+ }}
252
+ .prose ul {{ list-style-type: disc; }}
253
+ .prose ol {{ list-style-type: decimal; }}
254
+ .prose li {{
255
+ margin-bottom: 0.25rem;
256
+ }}
257
+ .prose code {{
258
+ background: #f3f3f3;
259
+ padding: 2px 4px;
260
+ border-radius: 4px;
261
+ font-family: 'Courier New', monospace;
262
+ font-size: 0.875rem;
263
+ }}
264
+ .prose pre {{
265
+ background: #f8f8f8;
266
+ padding: 1rem;
267
+ border-radius: 8px;
268
+ overflow-x: auto;
269
+ margin: 1rem 0;
270
+ border: 1px solid #e5e5e5;
271
+ }}
272
+ .prose pre code {{
273
+ background: none;
274
+ padding: 0;
275
+ }}
276
+ .prose blockquote {{
277
+ border-left: 4px solid #a859e4;
278
+ padding-left: 1rem;
279
+ margin: 1rem 0;
280
+ font-style: italic;
281
+ color: #666;
282
+ }}
283
+ </style>
284
+ </head>
285
+ <body class='font-inter bg-rtd-background'>
286
+ <div class='max-w-7xl mx-auto p-6 lg:p-8'>
287
+ <div class='bg-white shadow-xl rounded-lg'>
288
+ <!-- Header & Navigation -->
289
+ <div class='text-center py-8'>
290
+ <img src='assets/rges-pit_logo.png' alt='RGES-PIT Logo' class='w-48 mx-auto mb-6'>
291
+ <h1 class='text-4xl font-bold text-rtd-secondary text-center mb-2'>
292
+ Solution Dossier:
293
+ {solution.alias or solution.solution_id[:8] + '...'}
294
+ </h1>
295
+ <p class='text-lg text-gray-600 text-center mb-2'>
296
+ Model Type:
297
+ <span class='font-mono bg-gray-100 px-2 py-1 rounded'>{solution.model_type}</span>
298
+ </p>
299
+ <p class='text-xl text-rtd-accent text-center mb-4'>Event: {event.event_id} | Team: (
300
+ {submission.team_name or 'Not specified'} |
301
+ Tier: {submission.tier or 'Not specified'}
302
+ {commit_html}
303
+ )</p>
304
+ {f"<p class='text-lg text-gray-600 text-center mb-2'>UUID: {solution.solution_id}</p>"
305
+ if solution.alias else ""}
306
+ <nav class='flex justify-center space-x-4 mb-8'>
307
+ <a
308
+ href='{event.event_id}.html'
309
+ class='text-rtd-accent hover:underline'
310
+ >
311
+ &larr; Back to Event {event.event_id}
312
+ </a>
313
+ <a href='index.html' class='text-rtd-accent hover:underline'>&larr; Back to Dashboard</a>
314
+ </nav>
315
+ </div>
316
+
317
+ <hr class="border-t-4 border-rtd-accent my-8 mx-8">
318
+
319
+ <!-- Regex Start -->
320
+
321
+ <!-- Solution Overview & Notes -->
322
+ <section class='mb-10 px-8'>
323
+ <h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Solution Overview & Notes</h2>
324
+ <table class='w-full text-left table-auto border-collapse mb-4'>
325
+ <thead class='bg-rtd-primary text-rtd-secondary uppercase text-sm'>
326
+ <tr><th>Parameter</th><th>Value</th><th>Uncertainty</th></tr>
327
+ </thead>
328
+ <tbody class='text-rtd-text'>
329
+ {param_table}
330
+ </tbody>
331
+ </table>
332
+ <p class='text-rtd-text mt-4'>Higher-Order Effects: {hoe_str}</p>
333
+ <h3 class='text-xl font-semibold text-rtd-secondary mt-6 mb-2'>Participant's Detailed Notes</h3>
334
+ <div class='bg-gray-50 p-4 rounded-lg shadow-inner text-rtd-text prose max-w-none'>{notes_html}</div>
335
+ </section>
336
+ <!-- Lightcurve & Lens Plane Visuals -->
337
+ <section class='mb-10 px-8'>
338
+ <h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Lightcurve & Lens Plane Visuals</h2>
339
+ <div class='grid grid-cols-1 md:grid-cols-2 gap-6'>
340
+ <div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md'>
341
+ <img
342
+ src='{lc_plot}'
343
+ alt='Lightcurve Plot'
344
+ class='w-full h-auto rounded-md mb-2'
345
+ >
346
+ <p class="text-sm text-rtd-secondary">
347
+ Caption: Lightcurve fit for Solution
348
+ {solution.alias or solution.solution_id[:8] + '...'}
349
+ </p>
350
+ </div>
351
+ <div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md'>
352
+ <img src='{lens_plot}' alt='Lens Plane Plot' class='w-full h-auto rounded-md mb-2'>
353
+ <p class='text-sm text-rtd-secondary'>
354
+ Caption: Lens plane geometry for Solution
355
+ {solution.alias or solution.solution_id[:8] + '...'}
356
+ </p>
357
+ </div>
358
+ </div>
359
+ <p class='text-rtd-text mt-4 text-center'>
360
+ Posterior Samples:
361
+ {f"<a href='{posterior}' class='text-rtd-accent hover:underline'>"
362
+ f"Download Posterior Data</a>"
363
+ if posterior else ''}
364
+ </p>
365
+ </section>
366
+ <!-- Fit Statistics & Data Utilization -->
367
+ <section class='mb-10 px-8'>
368
+ <h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>
369
+ Fit Statistics & Data Utilization
370
+ </h2>
371
+ <div class='grid grid-cols-1 md:grid-cols-2 gap-6'>
372
+ <div class='bg-rtd-primary p-6 rounded-lg shadow-md text-center'>
373
+ <p class='text-sm font-medium text-rtd-secondary'>Log-Likelihood</p>
374
+ <p class='text-4xl font-bold text-rtd-accent mt-2'>
375
+ {solution.log_likelihood if solution.log_likelihood is not None else 'N/A'}
376
+ </p>
377
+ </div>
378
+ <div class='bg-rtd-primary p-6 rounded-lg shadow-md text-center'>
379
+ <p class='text-sm font-medium text-rtd-secondary'>N Data Points Used</p>
380
+ <p class='text-4xl font-bold text-rtd-accent mt-2'>
381
+ {solution.n_data_points if solution.n_data_points is not None else 'N/A'}
382
+ </p>
383
+ </div>
384
+ </div>
385
+ <h3 class='text-xl font-semibold text-rtd-secondary mt-6 mb-2'>
386
+ Data Utilization Ratio
387
+ </h3>
388
+ <div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md'>
389
+ <img
390
+ src='{DATA_UTILIZATION_URL}'
391
+ alt='Data Utilization'
392
+ class='w-full h-auto rounded-md mb-2'
393
+ >
394
+ <p class='text-sm text-rtd-secondary'>
395
+ Caption: Percentage of total event data points utilized in this solution's fit.
396
+ </p>
397
+ </div>
398
+ </section>
399
+ <!-- Compute Performance -->
400
+ <section class='mb-10 px-8'>
401
+ <h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>Compute Performance</h2>
402
+ <table class='w-full text-left table-auto border-collapse'>
403
+ <thead class='bg-rtd-primary text-rtd-secondary uppercase text-sm'>
404
+ <tr>
405
+ <th>Metric</th>
406
+ <th>Your Solution</th>
407
+ <th>Same-Team Average</th>
408
+ <th>All-Submission Average</th>
409
+ </tr>
410
+ </thead>
411
+ <tbody class='text-rtd-text'>
412
+ <tr>
413
+ <td>CPU Hours</td>
414
+ <td>
415
+ {solution.compute_info.get('cpu_hours', 'N/A') if solution.compute_info else 'N/A'}
416
+ </td>
417
+ <td>N/A for Participants</td><td>N/A for Participants</td>
418
+ </tr>
419
+ <tr>
420
+ <td>Wall Time (Hrs)</td>
421
+ <td>
422
+ {solution.compute_info.get('wall_time_hours', 'N/A')
423
+ if solution.compute_info else 'N/A'}
424
+ </td>
425
+ <td>N/A for Participants</td><td>N/A for Participants</td>
426
+ </tr>
427
+ </tbody>
428
+ </table>
429
+ <p class='text-sm text-gray-500 italic mt-4'>
430
+ Note: Comparison to other teams' compute times is available in the Evaluator Dossier.
431
+ </p>
432
+ </section>
433
+ <!-- Parameter Accuracy vs. Truths (Evaluator-Only) -->
434
+ <section class='mb-10 px-8'>
435
+ <h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>
436
+ Parameter Accuracy vs. Truths (Evaluator-Only)
437
+ </h2>
438
+ <p class='text-sm text-gray-500 italic mb-4'>
439
+ You haven't made a mistake. This just isn't for you.
440
+ Detailed comparisons of your fitted parameters against simulation truths
441
+ are available in the Evaluator Dossier.
442
+ </p>
443
+ <div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md'>
444
+ <img
445
+ src='{PARAM_COMPARISON_URL}'
446
+ alt='Parameter Comparison Table'
447
+ class='w-full h-auto rounded-md mb-2'
448
+ >
449
+ <p class='text-sm text-rtd-secondary'>
450
+ Caption: A table comparing fitted parameters to true values
451
+ (Evaluator View).
452
+ </p>
453
+ </div>
454
+ <div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md mt-6'>
455
+ <img
456
+ src='{PARAM_DIFFERENCE_URL}'
457
+ alt='Parameter Difference Distributions'
458
+ class='w-full h-auto rounded-md mb-2'
459
+ >
460
+ <p class='text-sm text-rtd-secondary'>
461
+ Caption: Distributions of (True - Fit) for key parameters across all
462
+ challenge submissions
463
+ (Evaluator View).
464
+ </p>
465
+ </div>
466
+ </section>
467
+ <!-- Physical Parameter Context (Evaluator-Only) -->
468
+ <section class='mb-10 px-8'>
469
+ <h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>
470
+ Physical Parameter Context (Evaluator-Only)
471
+ </h2>
472
+ <p class='text-sm text-gray-500 italic mb-4'>
473
+ You haven't made a mistake. This just isn't for you.
474
+ Contextual plots of derived physical parameters against population models
475
+ are available in the Evaluator Dossier.
476
+ </p>
477
+ <table class='w-full text-left table-auto border-collapse'>
478
+ <thead class='bg-rtd-primary text-rtd-secondary uppercase text-sm'>
479
+ <tr><th>Parameter</th><th>Value</th></tr>
480
+ </thead>
481
+ <tbody class='text-rtd-text'>
482
+ {phys_table}
483
+ </tbody>
484
+ </table>
485
+ <div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md mt-6'>
486
+ <img
487
+ src='{PHYSICAL_PARAM_URL}'
488
+ alt='Physical Parameter Distribution'
489
+ class='w-full h-auto rounded-md mb-2'
490
+ >
491
+ <p class='text-sm text-rtd-secondary'>
492
+ Caption: Your solution's derived physical parameters plotted against a simulated
493
+ test set
494
+ (Evaluator View).
495
+ </p>
496
+ </div>
497
+ </section>
498
+ <!-- Source Properties & CMD (Evaluator-Only) -->
499
+ <section class='mb-10 px-8'>
500
+ <h2 class='text-2xl font-semibold text-rtd-secondary mb-4'>
501
+ Source Properties & CMD (Evaluator-Only)
502
+ </h2>
503
+ <p class='text-sm text-gray-500 italic mb-4'>
504
+ You haven't made a mistake. This just isn't for you.
505
+ Source color and magnitude diagrams are available in the Evaluator Dossier.
506
+ </p>
507
+ <div class='text-rtd-text'>
508
+ <!-- Placeholder for source color/mag details -->
509
+ </div>
510
+ <div class='text-center bg-rtd-primary p-4 rounded-lg shadow-md mt-6'>
511
+ <img
512
+ src='{CMD_WITH_SOURCE_URL}'
513
+ alt='Color-Magnitude Diagram'
514
+ class='w-full h-auto rounded-md mb-2'
515
+ >
516
+ <p class='text-sm text-rtd-secondary'>
517
+ Caption: Color-Magnitude Diagram for the event's field with source marked
518
+ (Evaluator View).
519
+ </p>
520
+ </div>
521
+ </section>
522
+
523
+ <!-- Footer -->
524
+ <div class='text-sm text-gray-500 text-center pt-8 border-t border-gray-200 mt-10'>
525
+ Generated by microlens-submit v{__version__} on {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
526
+ </div>
527
+
528
+ <!-- Regex Finish -->
529
+
530
+ </div>
531
+ </div>
532
+ </body>
533
+ </html>"""
534
+ return html
@@ -0,0 +1,111 @@
1
+ """
2
+ Utility functions for dossier generation.
3
+
4
+ This module contains shared utility functions used across the dossier
5
+ generation package, including hardware formatting, GitHub URL parsing,
6
+ and other helper functions.
7
+ """
8
+
9
+ from typing import Any, Dict, Optional
10
+
11
+
12
+ def format_hardware_info(hardware_info: Optional[Dict[str, Any]]) -> str:
13
+ """Format hardware information for display in the dashboard.
14
+
15
+ Converts hardware information dictionary into a human-readable string
16
+ suitable for display in the dashboard. Handles various hardware info
17
+ formats and provides fallbacks for missing information.
18
+
19
+ Args:
20
+ hardware_info: Dictionary containing hardware information. Can include
21
+ keys like 'cpu_details', 'cpu', 'memory_gb', 'ram_gb', 'nexus_image'.
22
+ If None or empty, returns "No hardware information available".
23
+
24
+ Returns:
25
+ str: Formatted hardware information string for display.
26
+
27
+ Example:
28
+ >>> hardware_info = {
29
+ ... 'cpu_details': 'Intel Xeon E5-2680 v4',
30
+ ... 'memory_gb': 64,
31
+ ... 'nexus_image': 'roman-science-platform:latest'
32
+ ... }
33
+ >>> format_hardware_info(hardware_info)
34
+ 'CPU: Intel Xeon E5-2680 v4, RAM: 64GB, Platform: Roman Nexus'
35
+
36
+ >>> format_hardware_info(None)
37
+ 'No hardware information available'
38
+
39
+ >>> format_hardware_info({'custom_field': 'custom_value'})
40
+ 'custom_field: custom_value'
41
+
42
+ Note:
43
+ This function handles multiple hardware info formats for compatibility
44
+ with different submission sources. It prioritizes detailed CPU info
45
+ over basic CPU info and provides fallbacks for missing data.
46
+ """
47
+ if not hardware_info:
48
+ return "No hardware information available"
49
+
50
+ lines = []
51
+ for key, value in hardware_info.items():
52
+ if isinstance(value, dict):
53
+ lines.append(f"{key}:")
54
+ for sub_key, sub_value in value.items():
55
+ lines.append(f" {sub_key}: {sub_value}")
56
+ else:
57
+ lines.append(f"{key}: {value}")
58
+
59
+ return "\n".join(lines)
60
+
61
+
62
+ def extract_github_repo_name(repo_url: str) -> str:
63
+ """Extract owner/repo name from a GitHub URL.
64
+
65
+ Parses GitHub repository URLs to extract the owner and repository name
66
+ in a display-friendly format. Handles various GitHub URL formats including
67
+ HTTPS, SSH, and URLs with .git extension.
68
+
69
+ Args:
70
+ repo_url: GitHub repository URL (e.g., https://github.com/owner/repo,
71
+ git@github.com:owner/repo.git, etc.)
72
+
73
+ Returns:
74
+ str: Repository name in "owner/repo" format, or the original URL
75
+ if parsing fails.
76
+
77
+ Example:
78
+ >>> extract_github_repo_name("https://github.com/username/microlens-submit")
79
+ 'username/microlens-submit'
80
+
81
+ >>> extract_github_repo_name("git@github.com:username/microlens-submit.git")
82
+ 'username/microlens-submit'
83
+
84
+ >>> extract_github_repo_name("https://github.com/org/repo-name")
85
+ 'org/repo-name'
86
+
87
+ >>> extract_github_repo_name("invalid-url")
88
+ 'invalid-url'
89
+
90
+ Note:
91
+ This function uses regex to parse GitHub URLs and handles common
92
+ variations. If the URL doesn't match expected patterns, it returns
93
+ the original URL unchanged.
94
+ """
95
+ # Extract the repository name from the URL
96
+ if not repo_url:
97
+ return None
98
+
99
+ # Handle different URL formats
100
+ if "github.com" in repo_url:
101
+ # Extract username/repo from GitHub URL
102
+ parts = repo_url.rstrip("/").split("/")
103
+ if len(parts) >= 2:
104
+ return f"{parts[-2]}/{parts[-1]}"
105
+ elif "gitlab.com" in repo_url:
106
+ # Extract username/repo from GitLab URL
107
+ parts = repo_url.rstrip("/").split("/")
108
+ if len(parts) >= 2:
109
+ return f"{parts[-2]}/{parts[-1]}"
110
+
111
+ return None