microlens-submit 0.12.1__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.1.dist-info → microlens_submit-0.16.0.dist-info}/METADATA +54 -37
  26. microlens_submit-0.16.0.dist-info/RECORD +32 -0
  27. {microlens_submit-0.12.1.dist-info → microlens_submit-0.16.0.dist-info}/WHEEL +1 -1
  28. microlens_submit/api.py +0 -1274
  29. microlens_submit/cli.py +0 -1803
  30. microlens_submit/dossier.py +0 -1443
  31. microlens_submit-0.12.1.dist-info/RECORD +0 -13
  32. {microlens_submit-0.12.1.dist-info/licenses → microlens_submit-0.16.0.dist-info}/LICENSE +0 -0
  33. {microlens_submit-0.12.1.dist-info → microlens_submit-0.16.0.dist-info}/entry_points.txt +0 -0
  34. {microlens_submit-0.12.1.dist-info → microlens_submit-0.16.0.dist-info}/top_level.txt +0 -0
@@ -1,163 +1,13 @@
1
- """microlens-submit: A stateful submission toolkit for the Microlensing Data Challenge.
1
+ """Stateful tools for Microlensing Data Challenge submissions.
2
2
 
3
- This package provides a comprehensive toolkit for managing and submitting microlensing
4
- data challenge solutions. It offers both programmatic API access and command-line
5
- interface for creating, validating, and exporting microlensing submissions.
6
-
7
- **Core Features:**
8
- - Project initialization and management
9
- - Solution creation and parameter validation
10
- - Submission validation and completeness checking
11
- - HTML dossier generation with rich visualizations
12
- - Export functionality for challenge submission
13
- - Command-line interface for all operations
14
-
15
- **Main Components:**
16
- - Submission: Top-level container for all submission data
17
- - Event: Container for microlensing events and their solutions
18
- - Solution: Individual microlensing solutions with parameters and metadata
19
- - Validation: Comprehensive parameter and submission validation
20
- - Dossier: HTML report generation with Tailwind CSS styling
21
-
22
- **Quick Start:**
23
- >>> from microlens_submit import load, Submission
24
- >>>
25
- >>> # Load an existing project
26
- >>> submission = load("./my_project")
27
- >>>
28
- >>> # Or create a new submission
29
- >>> submission = Submission()
30
- >>> submission.team_name = "Team Alpha"
31
- >>> submission.tier = "advanced"
32
- >>> submission.save("./my_project")
33
- >>>
34
- >>> # Add a solution to an event
35
- >>> event = submission.get_event("EVENT001")
36
- >>> solution = event.add_solution(
37
- ... model_type="1S1L",
38
- ... parameters={"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
39
- ... )
40
- >>> solution.log_likelihood = -1234.56
41
- >>> solution.cpu_hours = 2.5
42
- >>> submission.save()
43
-
44
- **Command Line Usage:**
45
- # Initialize a new project
46
- microlens-submit init --team-name "Team Alpha" --tier "advanced" ./project
47
-
48
- # Add a solution
49
- microlens-submit add-solution EVENT001 1S1L ./project \
50
- --param t0=2459123.5 --param u0=0.1 --param tE=20.0 \
51
- --log-likelihood -1234.56 --cpu-hours 2.5
52
-
53
- # Validate and generate dossier
54
- microlens-submit validate-submission ./project
55
- microlens-submit generate-dossier ./project
56
-
57
- # Export for submission
58
- microlens-submit export submission.zip ./project
59
-
60
- **Supported Model Types:**
61
- - 1S1L: Point Source, Single Point Lens (standard microlensing)
62
- - 1S2L: Point Source, Binary Point Lens
63
- - 2S1L: Binary Source, Single Point Lens
64
- - 2S2L: Binary Source, Binary Point Lens
65
- - 1S3L: Point Source, Triple Point Lens
66
- - 2S3L: Binary Source, Triple Point Lens
67
-
68
- **Higher-Order Effects:**
69
- - parallax: Microlens parallax effect
70
- - finite-source: Finite source size effect
71
- - lens-orbital-motion: Orbital motion of lens components
72
- - xallarap: Source orbital motion
73
- - gaussian-process: Gaussian process noise modeling
74
- - stellar-rotation: Stellar rotation effects
75
- - fitted-limb-darkening: Fitted limb darkening coefficients
76
-
77
- Example:
78
- >>> from microlens_submit import load, Submission
79
- >>> from pathlib import Path
80
- >>>
81
- >>> # Create a new submission project
82
- >>> submission = Submission()
83
- >>> submission.team_name = "Team Alpha"
84
- >>> submission.tier = "advanced"
85
- >>> submission.repo_url = "https://github.com/team-alpha/microlens-analysis"
86
- >>>
87
- >>> # Add hardware information
88
- >>> submission.hardware_info = {
89
- ... "cpu_details": "Intel Xeon E5-2680 v4",
90
- ... "memory_gb": 64,
91
- ... "nexus_image": "roman-science-platform:latest"
92
- ... }
93
- >>>
94
- >>> # Create an event and add solutions
95
- >>> event = submission.get_event("EVENT001")
96
- >>>
97
- >>> # Simple 1S1L solution
98
- >>> solution1 = event.add_solution(
99
- ... model_type="1S1L",
100
- ... parameters={
101
- ... "t0": 2459123.5,
102
- ... "u0": 0.1,
103
- ... "tE": 20.0,
104
- ... "F0_S": 1000.0,
105
- ... "F0_B": 500.0
106
- ... }
107
- ... )
108
- >>> solution1.log_likelihood = -1234.56
109
- >>> solution1.n_data_points = 1250
110
- >>> solution1.cpu_hours = 2.5
111
- >>> solution1.relative_probability = 0.8
112
- >>> solution1.notes = "# Simple Point Lens Fit\n\nThis is a basic 1S1L solution."
113
- >>>
114
- >>> # Binary lens solution with parallax
115
- >>> solution2 = event.add_solution(
116
- ... model_type="1S2L",
117
- ... parameters={
118
- ... "t0": 2459123.5,
119
- ... "u0": 0.08,
120
- ... "tE": 22.1,
121
- ... "s": 1.15,
122
- ... "q": 0.001,
123
- ... "alpha": 45.2,
124
- ... "piEN": 0.1,
125
- ... "piEE": 0.05,
126
- ... "F0_S": 1000.0,
127
- ... "F0_B": 500.0
128
- ... }
129
- ... )
130
- >>> solution2.higher_order_effects = ["parallax"]
131
- >>> solution2.t_ref = 2459123.0
132
- >>> solution2.log_likelihood = -1189.34
133
- >>> solution2.cpu_hours = 15.2
134
- >>> solution2.relative_probability = 0.2
135
- >>>
136
- >>> # Save the submission
137
- >>> submission.save("./my_submission")
138
- >>>
139
- >>> # Validate the submission
140
- >>> warnings = submission.validate()
141
- >>> if warnings:
142
- ... print("Validation warnings:", warnings)
143
- >>> else:
144
- ... print("Submission is valid!")
145
- >>>
146
- >>> # Generate dossier
147
- >>> from microlens_submit.dossier import generate_dashboard_html
148
- >>> generate_dashboard_html(submission, Path("./my_submission/dossier"))
149
-
150
- Note:
151
- This package is designed for the Microlensing Data Challenge and provides
152
- comprehensive tools for managing submission data. All data is stored in
153
- JSON format for portability and human readability. The package includes
154
- extensive validation to ensure submission completeness and correctness.
155
- The HTML dossier generation creates professional, printable reports with
156
- Tailwind CSS styling and syntax-highlighted markdown notes.
3
+ ``microlens-submit`` manages events and solutions on disk so you can build,
4
+ validate, and export a challenge submission using either the Python API or
5
+ the command line interface.
157
6
  """
158
7
 
159
- __version__ = "0.12.1"
8
+ __version__ = "0.16.0"
160
9
 
161
- from .api import Event, Solution, Submission, load
10
+ from .models import Event, Solution, Submission
11
+ from .utils import load
162
12
 
163
13
  __all__ = ["Event", "Solution", "Submission", "load"]
@@ -0,0 +1,5 @@
1
+ """CLI package for microlens-submit."""
2
+
3
+ from .main import app
4
+
5
+ __all__ = ["app"]
@@ -0,0 +1,6 @@
1
+ """Entry point for running the CLI as a module."""
2
+
3
+ from .main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1 @@
1
+ """CLI commands package for microlens-submit."""
@@ -0,0 +1,139 @@
1
+ """Dossier generation commands for microlens-submit CLI."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+
10
+ from microlens_submit.dossier import generate_dashboard_html, generate_event_page, generate_solution_page
11
+ from microlens_submit.dossier.full_report import generate_full_dossier_report_html
12
+ from microlens_submit.utils import load
13
+
14
+ console = Console()
15
+
16
+
17
+ def generate_dossier(
18
+ project_path: Path = typer.Argument(Path("."), help="Project directory"),
19
+ event_id: Optional[str] = typer.Option(
20
+ None,
21
+ "--event-id",
22
+ help="Generate dossier for a specific event only (omit for full dossier)",
23
+ ),
24
+ solution_id: Optional[str] = typer.Option(
25
+ None,
26
+ "--solution-id",
27
+ help="Generate dossier for a specific solution only (omit for full dossier)",
28
+ ),
29
+ open: bool = typer.Option(
30
+ False,
31
+ "--open",
32
+ help="Open the generated dossier in your web browser after generation.",
33
+ ),
34
+ ) -> None:
35
+ """Generate an HTML dossier for the submission.
36
+
37
+ Use --open to automatically open the main dossier page in your browser after generation.
38
+ """
39
+ sub = load(str(project_path))
40
+ output_dir = Path(project_path) / "dossier"
41
+
42
+ if solution_id:
43
+ # Find the solution across all events (same pattern as other CLI commands)
44
+ solution = None
45
+ containing_event_id = None
46
+ for eid, event in sub.events.items():
47
+ if solution_id in event.solutions:
48
+ solution = event.solutions[solution_id]
49
+ containing_event_id = eid
50
+ break
51
+
52
+ if solution is None:
53
+ console.print(f"Solution {solution_id} not found", style="bold red")
54
+ raise typer.Exit(1)
55
+
56
+ # Create output directory and assets subdirectory
57
+ output_dir.mkdir(parents=True, exist_ok=True)
58
+ (output_dir / "assets").mkdir(exist_ok=True)
59
+
60
+ # Generate only the specific solution page
61
+ event = sub.events[containing_event_id]
62
+ console.print(
63
+ Panel(
64
+ f"Generating dossier for solution {solution_id} in event {containing_event_id}...",
65
+ style="cyan",
66
+ )
67
+ )
68
+ generate_solution_page(solution, event, sub, output_dir)
69
+ if open:
70
+ import webbrowser
71
+
72
+ solution_path = output_dir / f"{solution_id}.html"
73
+ if solution_path.exists():
74
+ webbrowser.open(solution_path.resolve().as_uri())
75
+
76
+ elif event_id:
77
+ # Generate only the specific event page
78
+ if event_id not in sub.events:
79
+ console.print(f"Event {event_id} not found", style="bold red")
80
+ raise typer.Exit(1)
81
+
82
+ # Create output directory and assets subdirectory
83
+ output_dir.mkdir(parents=True, exist_ok=True)
84
+ (output_dir / "assets").mkdir(exist_ok=True)
85
+
86
+ event = sub.events[event_id]
87
+ console.print(Panel(f"Generating dossier for event {event_id}...", style="cyan"))
88
+ generate_event_page(event, sub, output_dir)
89
+ if open:
90
+ import webbrowser
91
+
92
+ event_path = output_dir / f"{event_id}.html"
93
+ if event_path.exists():
94
+ webbrowser.open(event_path.resolve().as_uri())
95
+
96
+ else:
97
+ # Generate full dossier (all events and solutions)
98
+ console.print(
99
+ Panel(
100
+ "Generating comprehensive dossier for all events and solutions...",
101
+ style="cyan",
102
+ )
103
+ )
104
+ generate_dashboard_html(sub, output_dir)
105
+
106
+ # Generate comprehensive printable dossier
107
+ console.print(Panel("Generating comprehensive printable dossier...", style="cyan"))
108
+ generate_full_dossier_report_html(sub, output_dir)
109
+
110
+ # Replace placeholder in index.html with the real link
111
+ dashboard_path = output_dir / "index.html"
112
+ if dashboard_path.exists():
113
+ with dashboard_path.open("r", encoding="utf-8") as f:
114
+ dashboard_html = f.read()
115
+ dashboard_html = dashboard_html.replace(
116
+ "<!--FULL_DOSSIER_LINK_PLACEHOLDER-->",
117
+ '<div class="text-center">'
118
+ '<a href="./full_dossier_report.html" '
119
+ 'class="inline-block bg-rtd-accent text-white py-3 px-6 '
120
+ "rounded-lg shadow-md hover:bg-rtd-secondary "
121
+ 'transition-colors duration-200 text-lg font-semibold mt-8">'
122
+ "View Full Comprehensive Dossier (Printable)</a></div>",
123
+ )
124
+ with dashboard_path.open("w", encoding="utf-8") as f:
125
+ f.write(dashboard_html)
126
+ console.print(Panel("Comprehensive dossier generated!", style="bold green"))
127
+
128
+ # Open the main dashboard if requested
129
+ if open:
130
+ import webbrowser
131
+
132
+ webbrowser.open(dashboard_path.resolve().as_uri())
133
+
134
+ console.print(
135
+ Panel(
136
+ f"Dossier generated successfully at {output_dir / 'index.html'}",
137
+ style="bold green",
138
+ )
139
+ )
@@ -0,0 +1,177 @@
1
+ """Export commands for microlens-submit CLI."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+
10
+ from microlens_submit.utils import load
11
+
12
+ console = Console()
13
+
14
+
15
+ def export(
16
+ output_path: Path,
17
+ project_path: Path = typer.Argument(Path("."), help="Project directory"),
18
+ ) -> None:
19
+ """Generate a zip archive containing all active solutions.
20
+
21
+ This command creates a submission-ready archive. Unlike save operations,
22
+ export requires all validation checks to pass (complete team info,
23
+ hardware info, valid parameters, etc.) since this is for actual submission.
24
+
25
+ Args:
26
+ output_path: Path for the output zip file
27
+ project_path: Directory containing the submission project
28
+
29
+ Example:
30
+ # Export for submission (requires complete submission)
31
+ microlens-submit export submission.zip ./my_project
32
+
33
+ Note:
34
+ Export is strict and requires complete submissions. Use save operations
35
+ for saving incomplete work during development.
36
+ """
37
+ sub = load(str(project_path))
38
+ sub.export(str(output_path))
39
+ console.print(Panel(f"Exported submission to {output_path}", style="bold green"))
40
+
41
+
42
+ def remove_event(
43
+ event_id: str,
44
+ force: bool = typer.Option(False, "--force", help="Force removal even if event has saved solutions"),
45
+ project_path: Path = typer.Argument(Path("."), help="Project directory"),
46
+ ) -> None:
47
+ """Remove an entire event and all its solutions from the submission."""
48
+ submission = load(str(project_path))
49
+
50
+ if event_id not in submission.events:
51
+ typer.echo(f"❌ Event '{event_id}' not found in submission")
52
+ raise typer.Exit(1)
53
+
54
+ event = submission.events[event_id]
55
+ solution_count = len(event.solutions)
56
+
57
+ if not force:
58
+ typer.echo(f"⚠️ This will permanently remove event '{event_id}' " f"and all {solution_count} solutions.")
59
+ typer.echo(" This action cannot be undone.")
60
+ confirm = typer.confirm("Are you sure you want to continue?")
61
+ if not confirm:
62
+ typer.echo("❌ Operation cancelled")
63
+ raise typer.Exit(0)
64
+
65
+ try:
66
+ removed = submission.remove_event(event_id, force=force)
67
+ if removed:
68
+ typer.echo(f"✅ Removed event '{event_id}' and all {solution_count} solutions")
69
+ submission.save()
70
+ else:
71
+ typer.echo(f"❌ Failed to remove event '{event_id}'")
72
+ raise typer.Exit(1)
73
+ except ValueError as e:
74
+ typer.echo(f"❌ Cannot remove event: {e}")
75
+ raise typer.Exit(1)
76
+
77
+
78
+ def set_repo_url(
79
+ repo_url: str = typer.Argument(..., help="GitHub repository URL (e.g. https://github.com/owner/repo)"),
80
+ project_path: Path = typer.Argument(Path("."), help="Project directory"),
81
+ ) -> None:
82
+ """Set or update the GitHub repository URL in the submission metadata."""
83
+ sub = load(str(project_path))
84
+ sub.repo_url = repo_url
85
+ sub.save()
86
+ console.print(
87
+ Panel(
88
+ f"Set repo_url to {repo_url} in {project_path}/submission.json",
89
+ style="bold green",
90
+ )
91
+ )
92
+
93
+
94
+ def set_hardware_info(
95
+ cpu: Optional[str] = typer.Option(None, "--cpu", help="CPU model/description"),
96
+ cpu_details: Optional[str] = typer.Option(None, "--cpu-details", help="Detailed CPU information"),
97
+ memory_gb: Optional[float] = typer.Option(None, "--memory-gb", help="Memory in GB"),
98
+ ram_gb: Optional[float] = typer.Option(None, "--ram-gb", help="RAM in GB (alternative to --memory-gb)"),
99
+ platform: Optional[str] = typer.Option(
100
+ None,
101
+ "--platform",
102
+ help="Platform description (e.g., 'Local Analysis', 'Roman Nexus')",
103
+ ),
104
+ nexus_image: Optional[str] = typer.Option(None, "--nexus-image", help="Roman Nexus image identifier"),
105
+ clear: bool = typer.Option(False, "--clear", help="Clear all existing hardware info"),
106
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be changed without saving"),
107
+ project_path: Path = typer.Argument(Path("."), help="Project directory"),
108
+ ) -> None:
109
+ """Set or update hardware information in the submission metadata."""
110
+ sub = load(str(project_path))
111
+
112
+ # Initialize hardware_info if it doesn't exist
113
+ if sub.hardware_info is None:
114
+ sub.hardware_info = {}
115
+
116
+ changes = []
117
+
118
+ # Clear existing info if requested
119
+ if clear:
120
+ if sub.hardware_info:
121
+ changes.append("Clear all existing hardware info")
122
+ sub.hardware_info = {}
123
+
124
+ # Set new values
125
+ if cpu_details is not None:
126
+ if sub.hardware_info.get("cpu_details") != cpu_details:
127
+ changes.append(f"Set cpu_details: {cpu_details}")
128
+ sub.hardware_info["cpu_details"] = cpu_details
129
+ elif cpu is not None:
130
+ if sub.hardware_info.get("cpu") != cpu:
131
+ changes.append(f"Set cpu: {cpu}")
132
+ sub.hardware_info["cpu"] = cpu
133
+
134
+ if memory_gb is not None:
135
+ if sub.hardware_info.get("memory_gb") != memory_gb:
136
+ changes.append(f"Set memory_gb: {memory_gb}")
137
+ sub.hardware_info["memory_gb"] = memory_gb
138
+ elif ram_gb is not None:
139
+ if sub.hardware_info.get("ram_gb") != ram_gb:
140
+ changes.append(f"Set ram_gb: {ram_gb}")
141
+ sub.hardware_info["ram_gb"] = ram_gb
142
+
143
+ if platform is not None:
144
+ if sub.hardware_info.get("platform") != platform:
145
+ changes.append(f"Set platform: {platform}")
146
+ sub.hardware_info["platform"] = platform
147
+
148
+ if nexus_image is not None:
149
+ if sub.hardware_info.get("nexus_image") != nexus_image:
150
+ changes.append(f"Set nexus_image: {nexus_image}")
151
+ sub.hardware_info["nexus_image"] = nexus_image
152
+
153
+ # Show dry run results
154
+ if dry_run:
155
+ if changes:
156
+ console.print(Panel("Hardware info changes (dry run):", style="cyan"))
157
+ for change in changes:
158
+ console.print(f" • {change}")
159
+ console.print(f"\nNew hardware_info: {sub.hardware_info}")
160
+ else:
161
+ console.print(Panel("No changes would be made", style="yellow"))
162
+ return
163
+
164
+ # Apply changes
165
+ if changes:
166
+ sub.save()
167
+ console.print(
168
+ Panel(
169
+ f"Updated hardware info in {project_path}/submission.json",
170
+ style="bold green",
171
+ )
172
+ )
173
+ for change in changes:
174
+ console.print(f" • {change}")
175
+ console.print(f"\nCurrent hardware_info: {sub.hardware_info}")
176
+ else:
177
+ console.print(Panel("No changes made", style="yellow"))
@@ -0,0 +1,172 @@
1
+ """Initialization commands for microlens-submit CLI."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+
10
+ from microlens_submit.utils import load
11
+
12
+ console = Console()
13
+
14
+
15
+ def init(
16
+ team_name: str = typer.Option(..., help="Team name"),
17
+ tier: str = typer.Option(..., help="Challenge tier"),
18
+ project_path: Path = typer.Argument(Path("."), help="Project directory"),
19
+ ) -> None:
20
+ """Create a new submission project in the specified directory.
21
+
22
+ Initializes a new microlensing submission project with the given team name
23
+ and tier. The project directory structure is created automatically, and
24
+ the submission.json file is initialized with basic metadata.
25
+
26
+ This command also attempts to auto-detect the GitHub repository URL
27
+ from the current git configuration and provides helpful feedback.
28
+
29
+ Args:
30
+ team_name: Name of the participating team (e.g., "Team Alpha").
31
+ tier: Challenge tier level (e.g., "basic", "advanced").
32
+ project_path: Directory where the project will be created.
33
+ Defaults to current directory if not specified.
34
+
35
+ Raises:
36
+ OSError: If unable to create the project directory or write files.
37
+
38
+ Example:
39
+ # Create project in current directory
40
+ microlens-submit init --team-name "Team Alpha" --tier "advanced"
41
+
42
+ # Create project in specific directory
43
+ microlens-submit init --team-name "Team Beta" --tier "basic" ./my_submission
44
+
45
+ # Project structure created:
46
+ # ./my_submission/
47
+ # ├── submission.json
48
+ # └── events/
49
+
50
+ Note:
51
+ If the project directory already exists, it will be used as-is.
52
+ If a git repository is detected, the GitHub URL will be automatically
53
+ set. Otherwise, a warning is shown and you can set it later with
54
+ set-repo-url command.
55
+ """
56
+ # Validate tier
57
+ try:
58
+ from microlens_submit.tier_validation import get_available_tiers, get_tier_description
59
+
60
+ available_tiers = get_available_tiers()
61
+ if tier not in available_tiers:
62
+ console.print(f"[yellow]Warning: Invalid tier '{tier}'[/yellow]")
63
+ console.print(f"Available tiers: {', '.join(available_tiers)}")
64
+ console.print("[yellow]Setting tier to 'None' (no validation)[/yellow]")
65
+ tier = "None"
66
+ tier_desc = get_tier_description(tier)
67
+ console.print(f"[green]Using tier:[/green] {tier} - {tier_desc}")
68
+ except ImportError:
69
+ # Tier validation module not available, skip validation
70
+ console.print(f"[yellow]Warning: Tier validation not available, using tier '{tier}'[/yellow]")
71
+ except ValueError as e:
72
+ console.print(f"[yellow]Warning: {e}[/yellow]")
73
+ console.print("[yellow]Setting tier to 'None' (no validation)[/yellow]")
74
+ tier = "None"
75
+
76
+ sub = load(str(project_path))
77
+ sub.team_name = team_name
78
+ sub.tier = tier
79
+ # Try to auto-detect repo_url
80
+ try:
81
+ repo_url = (
82
+ subprocess.check_output(
83
+ ["git", "config", "--get", "remote.origin.url"],
84
+ stderr=subprocess.DEVNULL,
85
+ )
86
+ .decode()
87
+ .strip()
88
+ )
89
+ except Exception:
90
+ repo_url = None
91
+ if repo_url:
92
+ sub.repo_url = repo_url
93
+ console.print(f"[green]Auto-detected GitHub repo URL:[/green] {repo_url}")
94
+ else:
95
+ console.print(
96
+ "[yellow]Could not auto-detect a GitHub repository URL. "
97
+ "Please add it using 'microlens-submit set-repo-url <url> "
98
+ "<project_dir>'.[/yellow]"
99
+ )
100
+
101
+ # Run warnings-only validation
102
+ warnings = sub.run_validation_warnings()
103
+ if warnings:
104
+ console.print("[yellow]⚠️ Project initialized with warnings:[/yellow]")
105
+ for warning in warnings:
106
+ console.print(f" [yellow]• {warning}[/yellow]")
107
+ console.print("[yellow]💡 These warnings will become errors when saving or exporting.[/yellow]")
108
+
109
+ # Try to save, but don't fail if there are validation errors
110
+ try:
111
+ sub.save()
112
+ console.print(Panel(f"Initialized project at {project_path}", style="bold green"))
113
+ except ValueError as e:
114
+ console.print("[yellow]⚠️ Project initialized but could not save due to validation errors:[/yellow]")
115
+ console.print(f"[yellow]{str(e)}[/yellow]")
116
+ console.print("[yellow]💡 Fix validation errors before saving or exporting.[/yellow]")
117
+ console.print(Panel(f"Initialized project at {project_path} (unsaved)", style="bold yellow"))
118
+
119
+
120
+ def nexus_init(
121
+ team_name: str = typer.Option(..., help="Team name"),
122
+ tier: str = typer.Option(..., help="Challenge tier"),
123
+ project_path: Path = typer.Argument(Path("."), help="Project directory"),
124
+ ) -> None:
125
+ """Create a project and record Roman Nexus environment details.
126
+
127
+ This command combines the functionality of init() with automatic
128
+ detection of Roman Science Platform environment information. It
129
+ populates hardware_info with CPU details, memory information, and
130
+ the Nexus image identifier.
131
+
132
+ Args:
133
+ team_name: Name of the participating team (e.g., "Team Alpha").
134
+ tier: Challenge tier level (e.g., "basic", "advanced").
135
+ project_path: Directory where the project will be created.
136
+ Defaults to current directory if not specified.
137
+
138
+ Example:
139
+ # Initialize project with Nexus platform info
140
+ microlens-submit nexus-init --team-name "Team Alpha" --tier "advanced" ./project
141
+
142
+ # This will automatically detect:
143
+ # - CPU model from /proc/cpuinfo
144
+ # - Memory from /proc/meminfo
145
+ # - Nexus image from JUPYTER_IMAGE_SPEC
146
+
147
+ Note:
148
+ This command is specifically designed for the Roman Science Platform
149
+ environment. It will silently skip any environment information that
150
+ cannot be detected (e.g., if running outside of Nexus).
151
+ """
152
+ init(team_name=team_name, tier=tier, project_path=project_path)
153
+ sub = load(str(project_path))
154
+ sub.autofill_nexus_info()
155
+
156
+ # Run warnings-only validation after adding hardware info
157
+ warnings = sub.run_validation_warnings()
158
+ if warnings:
159
+ console.print("[yellow]⚠️ Project updated with warnings:[/yellow]")
160
+ for warning in warnings:
161
+ console.print(f" [yellow]• {warning}[/yellow]")
162
+ console.print("[yellow]💡 These warnings will become errors when saving or exporting.[/yellow]")
163
+
164
+ # Try to save, but don't fail if there are validation errors
165
+ try:
166
+ sub.save()
167
+ console.print("Nexus platform info captured.", style="bold green")
168
+ except ValueError as e:
169
+ console.print("[yellow]⚠️ Project updated but could not save due to validation errors:[/yellow]")
170
+ console.print(f"[yellow]{str(e)}[/yellow]")
171
+ console.print("[yellow]💡 Fix validation errors before saving or exporting.[/yellow]")
172
+ console.print("Nexus platform info captured (unsaved).", style="bold yellow")