microlens-submit 0.12.2__py3-none-any.whl → 0.16.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.
- microlens_submit/__init__.py +7 -157
- microlens_submit/cli/__init__.py +5 -0
- microlens_submit/cli/__main__.py +6 -0
- microlens_submit/cli/commands/__init__.py +1 -0
- microlens_submit/cli/commands/dossier.py +139 -0
- microlens_submit/cli/commands/export.py +177 -0
- microlens_submit/cli/commands/init.py +172 -0
- microlens_submit/cli/commands/solutions.py +722 -0
- microlens_submit/cli/commands/validation.py +241 -0
- microlens_submit/cli/main.py +120 -0
- microlens_submit/dossier/__init__.py +51 -0
- microlens_submit/dossier/dashboard.py +499 -0
- microlens_submit/dossier/event_page.py +369 -0
- microlens_submit/dossier/full_report.py +330 -0
- microlens_submit/dossier/solution_page.py +533 -0
- microlens_submit/dossier/utils.py +111 -0
- microlens_submit/error_messages.py +283 -0
- microlens_submit/models/__init__.py +28 -0
- microlens_submit/models/event.py +406 -0
- microlens_submit/models/solution.py +569 -0
- microlens_submit/models/submission.py +569 -0
- microlens_submit/tier_validation.py +208 -0
- microlens_submit/utils.py +373 -0
- microlens_submit/validate_parameters.py +478 -180
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/METADATA +42 -27
- microlens_submit-0.16.0.dist-info/RECORD +32 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/WHEEL +1 -1
- microlens_submit/api.py +0 -1257
- microlens_submit/cli.py +0 -1803
- microlens_submit/dossier.py +0 -1443
- microlens_submit-0.12.2.dist-info/RECORD +0 -13
- {microlens_submit-0.12.2.dist-info/licenses → microlens_submit-0.16.0.dist-info}/LICENSE +0 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/entry_points.txt +0 -0
- {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Full dossier report generation module for microlens-submit.
|
|
3
|
+
|
|
4
|
+
This module provides functionality to generate comprehensive printable HTML
|
|
5
|
+
dossier reports that combine all dashboard, event, and solution content
|
|
6
|
+
into a single document.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ..models import Submission
|
|
14
|
+
from ..models.solution import Solution
|
|
15
|
+
from .dashboard import _generate_dashboard_content
|
|
16
|
+
from .event_page import _generate_event_page_content
|
|
17
|
+
from .solution_page import _generate_solution_page_content
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate_full_dossier_report_html(submission: Submission, output_dir: Path) -> None:
|
|
21
|
+
"""Generate a comprehensive printable HTML dossier report.
|
|
22
|
+
|
|
23
|
+
Creates a single HTML file that concatenates all dossier sections (dashboard,
|
|
24
|
+
events, and solutions) into one comprehensive, printable document. This is
|
|
25
|
+
useful for creating a complete submission overview that can be printed or
|
|
26
|
+
shared as a single file.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
submission: The submission object containing all events and solutions.
|
|
30
|
+
output_dir: Directory where the full dossier report will be saved.
|
|
31
|
+
The file will be named full_dossier_report.html.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
OSError: If unable to write the HTML file.
|
|
35
|
+
ValueError: If submission data is invalid or extraction fails.
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> from microlens_submit import load
|
|
39
|
+
>>> from microlens_submit.dossier import generate_full_dossier_report_html
|
|
40
|
+
>>> from pathlib import Path
|
|
41
|
+
>>>
|
|
42
|
+
>>> submission = load("./my_project")
|
|
43
|
+
>>>
|
|
44
|
+
>>> # Generate comprehensive dossier
|
|
45
|
+
>>> generate_full_dossier_report_html(submission, Path("./dossier_output"))
|
|
46
|
+
>>>
|
|
47
|
+
>>> # Creates: ./dossier_output/full_dossier_report.html
|
|
48
|
+
>>> # This file contains all dashboard, event, and solution content
|
|
49
|
+
>>> # in a single, printable HTML document
|
|
50
|
+
|
|
51
|
+
Note:
|
|
52
|
+
This function creates a comprehensive report by extracting content from
|
|
53
|
+
individual pages and combining them with section dividers. The report
|
|
54
|
+
includes all active solutions and maintains the same styling as
|
|
55
|
+
individual pages. This is typically called automatically by
|
|
56
|
+
generate_dashboard_html() when creating a full dossier.
|
|
57
|
+
"""
|
|
58
|
+
all_html_sections = []
|
|
59
|
+
# Dashboard (extract only main content, skip header/logo)
|
|
60
|
+
dash_html = _generate_dashboard_content(submission, full_dossier_exists=True)
|
|
61
|
+
dash_body = extract_main_content_body(dash_html)
|
|
62
|
+
all_html_sections.append(dash_body)
|
|
63
|
+
all_html_sections.append('<hr class="my-8 border-t-2 border-rtd-accent">') # Divider after dashboard
|
|
64
|
+
|
|
65
|
+
# Events and solutions
|
|
66
|
+
for event in submission.events.values():
|
|
67
|
+
event_html = _generate_event_page_content(event, submission)
|
|
68
|
+
event_body = extract_main_content_body(event_html, section_type="event", section_id=event.event_id)
|
|
69
|
+
all_html_sections.append(event_body)
|
|
70
|
+
all_html_sections.append('<hr class="my-8 border-t-2 border-rtd-accent">') # Divider after event
|
|
71
|
+
|
|
72
|
+
for sol in event.get_active_solutions():
|
|
73
|
+
sol_html = _generate_solution_page_content(sol, event, submission)
|
|
74
|
+
sol_body = extract_main_content_body(
|
|
75
|
+
sol_html,
|
|
76
|
+
section_type="solution",
|
|
77
|
+
section_id=sol.solution_id,
|
|
78
|
+
project_root=Path(submission.project_path),
|
|
79
|
+
solution=sol,
|
|
80
|
+
)
|
|
81
|
+
all_html_sections.append(sol_body)
|
|
82
|
+
all_html_sections.append('<hr class="my-8 border-t-2 border-rtd-accent">') # Divider after solution
|
|
83
|
+
|
|
84
|
+
# Compose the full HTML
|
|
85
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
86
|
+
header = f"""
|
|
87
|
+
<div class="text-center py-8 bg-rtd-primary text-rtd-secondary">
|
|
88
|
+
<img src='assets/rges-pit_logo.png' alt='RGES-PIT Logo' class='w-48 mx-auto mb-6'>
|
|
89
|
+
<h1 class="text-3xl font-bold mb-2">Comprehensive Submission Dossier</h1>
|
|
90
|
+
<p class="text-lg">Generated on: {now}</p>
|
|
91
|
+
<p class="text-md">
|
|
92
|
+
Team: {submission.team_name} | Tier: {submission.tier}
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
<hr class="border-t-4 border-rtd-accent my-8">
|
|
96
|
+
"""
|
|
97
|
+
html = f"""<!DOCTYPE html>
|
|
98
|
+
<html lang="en">
|
|
99
|
+
<head>
|
|
100
|
+
<meta charset="UTF-8">
|
|
101
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
102
|
+
<title>Full Dossier Report - {submission.team_name}</title>
|
|
103
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
104
|
+
<script>
|
|
105
|
+
tailwind.config = {{
|
|
106
|
+
theme: {{
|
|
107
|
+
extend: {{
|
|
108
|
+
colors: {{
|
|
109
|
+
'rtd-primary': '#dfc5fa',
|
|
110
|
+
'rtd-secondary': '#361d49',
|
|
111
|
+
'rtd-accent': '#a859e4',
|
|
112
|
+
'rtd-background': '#faf7fd',
|
|
113
|
+
'rtd-text': '#000',
|
|
114
|
+
}},
|
|
115
|
+
fontFamily: {{
|
|
116
|
+
inter: ['Inter', 'sans-serif'],
|
|
117
|
+
}},
|
|
118
|
+
}},
|
|
119
|
+
}},
|
|
120
|
+
}};
|
|
121
|
+
</script>
|
|
122
|
+
<link
|
|
123
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
|
|
124
|
+
rel="stylesheet"
|
|
125
|
+
>
|
|
126
|
+
<!-- Highlight.js for code syntax highlighting -->
|
|
127
|
+
<link
|
|
128
|
+
rel="stylesheet"
|
|
129
|
+
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css"
|
|
130
|
+
>
|
|
131
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js">
|
|
132
|
+
</script>
|
|
133
|
+
<script>hljs.highlightAll();</script>
|
|
134
|
+
<style>
|
|
135
|
+
.prose {{
|
|
136
|
+
color: #000;
|
|
137
|
+
line-height: 1.6;
|
|
138
|
+
}}
|
|
139
|
+
.prose h1 {{
|
|
140
|
+
font-size: 1.5rem;
|
|
141
|
+
font-weight: 700;
|
|
142
|
+
color: #361d49;
|
|
143
|
+
margin-top: 1.5rem;
|
|
144
|
+
margin-bottom: 0.75rem;
|
|
145
|
+
}}
|
|
146
|
+
.prose h2 {{
|
|
147
|
+
font-size: 1.25rem;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
color: #361d49;
|
|
150
|
+
margin-top: 1.25rem;
|
|
151
|
+
margin-bottom: 0.5rem;
|
|
152
|
+
}}
|
|
153
|
+
.prose h3 {{
|
|
154
|
+
font-size: 1.125rem;
|
|
155
|
+
font-weight: 600;
|
|
156
|
+
color: #a859e4;
|
|
157
|
+
margin-top: 1rem;
|
|
158
|
+
margin-bottom: 0.5rem;
|
|
159
|
+
}}
|
|
160
|
+
.prose p {{
|
|
161
|
+
margin-bottom: 0.75rem;
|
|
162
|
+
}}
|
|
163
|
+
.prose ul, .prose ol {{
|
|
164
|
+
margin-left: 1.5rem;
|
|
165
|
+
margin-bottom: 0.75rem;
|
|
166
|
+
}}
|
|
167
|
+
.prose ul {{ list-style-type: disc; }}
|
|
168
|
+
.prose ol {{ list-style-type: decimal; }}
|
|
169
|
+
.prose li {{
|
|
170
|
+
margin-bottom: 0.25rem;
|
|
171
|
+
}}
|
|
172
|
+
.prose code {{
|
|
173
|
+
background: #f3f3f3;
|
|
174
|
+
padding: 2px 4px;
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
font-family: 'Courier New', monospace;
|
|
177
|
+
font-size: 0.875rem;
|
|
178
|
+
}}
|
|
179
|
+
.prose pre {{
|
|
180
|
+
background: #f8f8f8;
|
|
181
|
+
padding: 1rem;
|
|
182
|
+
border-radius: 8px;
|
|
183
|
+
overflow-x: auto;
|
|
184
|
+
margin: 1rem 0;
|
|
185
|
+
border: 1px solid #e5e5e5;
|
|
186
|
+
}}
|
|
187
|
+
.prose pre code {{
|
|
188
|
+
background: none;
|
|
189
|
+
padding: 0;
|
|
190
|
+
}}
|
|
191
|
+
.prose blockquote {{
|
|
192
|
+
border-left: 4px solid #a859e4;
|
|
193
|
+
padding-left: 1rem;
|
|
194
|
+
margin: 1rem 0;
|
|
195
|
+
font-style: italic;
|
|
196
|
+
color: #666;
|
|
197
|
+
}}
|
|
198
|
+
</style>
|
|
199
|
+
</head>
|
|
200
|
+
<body class="font-inter bg-rtd-background">
|
|
201
|
+
<div class="max-w-7xl mx-auto p-6 lg:p-8">
|
|
202
|
+
{header}
|
|
203
|
+
{''.join(all_html_sections)}
|
|
204
|
+
</div>
|
|
205
|
+
</body>
|
|
206
|
+
</html>"""
|
|
207
|
+
with (output_dir / "full_dossier_report.html").open("w", encoding="utf-8") as f:
|
|
208
|
+
f.write(html)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def extract_main_content_body(
|
|
212
|
+
html: str,
|
|
213
|
+
section_type: str = None,
|
|
214
|
+
section_id: str = None,
|
|
215
|
+
project_root: Path = None,
|
|
216
|
+
solution: Solution = None,
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Extract main content for the full dossier using explicit markers.
|
|
219
|
+
|
|
220
|
+
Extracts the main content from HTML pages using explicit marker comments.
|
|
221
|
+
This function is used to create the comprehensive full dossier report by
|
|
222
|
+
extracting content from individual pages and combining them.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
html: The complete HTML content to extract from.
|
|
226
|
+
section_type: Type of section being extracted. If None, extracts dashboard
|
|
227
|
+
content. If 'event' or 'solution', extracts and formats accordingly.
|
|
228
|
+
section_id: Identifier for the section (event_id or solution_id). Used
|
|
229
|
+
to create section headings in the full dossier.
|
|
230
|
+
project_root: Path to the project root directory. Used to access aliases.json
|
|
231
|
+
for solution alias lookups.
|
|
232
|
+
solution: Solution object (required when section_type is 'solution'). Used
|
|
233
|
+
to get model_type and other solution metadata for the heading.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
str: Extracted and formatted HTML content ready for inclusion in
|
|
237
|
+
the full dossier report.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
ValueError: If required regex markers are not found in the HTML.
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
>>> # Extract dashboard content
|
|
244
|
+
>>> dashboard_html = _generate_dashboard_content(submission)
|
|
245
|
+
>>> dashboard_body = extract_main_content_body(dashboard_html)
|
|
246
|
+
>>>
|
|
247
|
+
>>> # Extract event content
|
|
248
|
+
>>> event_html = _generate_event_page_content(event, submission)
|
|
249
|
+
>>> event_body = extract_main_content_body(event_html, 'event', 'EVENT001')
|
|
250
|
+
>>>
|
|
251
|
+
>>> # Extract solution content
|
|
252
|
+
>>> solution_html = _generate_solution_page_content(solution, event, submission)
|
|
253
|
+
>>> solution_body = extract_main_content_body(solution_html, 'solution', 'sol_uuid', project_root, solution)
|
|
254
|
+
|
|
255
|
+
Note:
|
|
256
|
+
This function relies on HTML comments <!-- Regex Start --> and
|
|
257
|
+
<!-- Regex Finish --> to identify content boundaries. These markers
|
|
258
|
+
must be present in the source HTML for extraction to work.
|
|
259
|
+
"""
|
|
260
|
+
if section_type is None: # dashboard
|
|
261
|
+
# Extract everything between the markers
|
|
262
|
+
start_marker = "<!-- Regex Start -->"
|
|
263
|
+
finish_marker = "<!-- Regex Finish -->"
|
|
264
|
+
|
|
265
|
+
start_pos = html.find(start_marker)
|
|
266
|
+
finish_pos = html.find(finish_marker)
|
|
267
|
+
|
|
268
|
+
if start_pos == -1 or finish_pos == -1:
|
|
269
|
+
raise ValueError("Could not find regex markers in dashboard HTML")
|
|
270
|
+
|
|
271
|
+
# Extract content between markers (including the markers themselves)
|
|
272
|
+
content = html[start_pos : finish_pos + len(finish_marker)]
|
|
273
|
+
|
|
274
|
+
# Remove the markers
|
|
275
|
+
content = content.replace(start_marker, "").replace(finish_marker, "")
|
|
276
|
+
|
|
277
|
+
return content.strip()
|
|
278
|
+
else:
|
|
279
|
+
# For event/solution: extract content between markers, remove header/nav/logo, add heading, wrap in <section>
|
|
280
|
+
start_marker = "<!-- Regex Start -->"
|
|
281
|
+
finish_marker = "<!-- Regex Finish -->"
|
|
282
|
+
|
|
283
|
+
start_pos = html.find(start_marker)
|
|
284
|
+
finish_pos = html.find(finish_marker)
|
|
285
|
+
|
|
286
|
+
if start_pos == -1 or finish_pos == -1:
|
|
287
|
+
raise ValueError("Could not find regex markers in HTML")
|
|
288
|
+
|
|
289
|
+
# Extract content between markers
|
|
290
|
+
content = html[start_pos : finish_pos + len(finish_marker)]
|
|
291
|
+
|
|
292
|
+
# Remove the markers
|
|
293
|
+
content = content.replace(start_marker, "").replace(finish_marker, "")
|
|
294
|
+
|
|
295
|
+
# Optionally add a heading
|
|
296
|
+
heading = ""
|
|
297
|
+
section_class = ""
|
|
298
|
+
if section_type == "event" and section_id:
|
|
299
|
+
heading = f'<h2 class="text-3xl font-bold text-rtd-accent my-8">Event: {section_id}</h2>'
|
|
300
|
+
section_class = "dossier-event-section"
|
|
301
|
+
elif section_type == "solution" and section_id:
|
|
302
|
+
# Look up alias from aliases.json if project_root is provided
|
|
303
|
+
alias_key = None
|
|
304
|
+
if project_root:
|
|
305
|
+
aliases_file = project_root / "aliases.json"
|
|
306
|
+
if aliases_file.exists():
|
|
307
|
+
try:
|
|
308
|
+
with aliases_file.open("r", encoding="utf-8") as f:
|
|
309
|
+
aliases = json.load(f)
|
|
310
|
+
# Look up the solution_id in the aliases
|
|
311
|
+
for key, uuid in aliases.items():
|
|
312
|
+
if uuid == section_id:
|
|
313
|
+
alias_key = key
|
|
314
|
+
break
|
|
315
|
+
except (json.JSONDecodeError, KeyError):
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
# Get model type from solution object
|
|
319
|
+
model_type = solution.model_type if solution else "Unknown"
|
|
320
|
+
|
|
321
|
+
if alias_key:
|
|
322
|
+
heading = f"""<h2 class="text-3xl font-bold text-rtd-accent my-6">Solution: {alias_key}</h2>
|
|
323
|
+
<h3 class="text-lg text-gray-600 mb-4">Model Type: {model_type} | UUID: {section_id}</h3>"""
|
|
324
|
+
else:
|
|
325
|
+
heading = f"""<h2 class="text-3xl font-bold text-rtd-accent my-6">Solution: {section_id}</h2>
|
|
326
|
+
<h3 class="text-lg text-gray-600 mb-4">Model Type: {model_type}</h3>"""
|
|
327
|
+
section_class = "dossier-solution-section"
|
|
328
|
+
|
|
329
|
+
# Wrap in a section for clarity
|
|
330
|
+
return f'<section class="{section_class}">\n{heading}\n{content.strip()}\n</section>'
|