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.
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 +499 -0
  13. microlens_submit/dossier/event_page.py +369 -0
  14. microlens_submit/dossier/full_report.py +330 -0
  15. microlens_submit/dossier/solution_page.py +533 -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.0.dist-info}/METADATA +42 -27
  26. microlens_submit-0.16.0.dist-info/RECORD +32 -0
  27. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/WHEEL +1 -1
  28. microlens_submit/api.py +0 -1257
  29. microlens_submit/cli.py +0 -1803
  30. microlens_submit/dossier.py +0 -1443
  31. microlens_submit-0.12.2.dist-info/RECORD +0 -13
  32. {microlens_submit-0.12.2.dist-info/licenses → microlens_submit-0.16.0.dist-info}/LICENSE +0 -0
  33. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.0.dist-info}/entry_points.txt +0 -0
  34. {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>'