microlens-submit 0.16.3__tar.gz → 0.16.5__tar.gz
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-0.16.3 → microlens_submit-0.16.5}/CHANGELOG.md +32 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/CITATION.cff +2 -2
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/PKG-INFO +17 -8
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/README.md +15 -7
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/__init__.py +1 -1
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/commands/export.py +49 -8
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/commands/init.py +16 -12
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/commands/solutions.py +87 -34
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/main.py +2 -1
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/dossier/event_page.py +20 -4
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/dossier/full_report.py +42 -3
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/dossier/solution_page.py +44 -5
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/dossier/utils.py +63 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/error_messages.py +2 -1
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/models/solution.py +6 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/models/submission.py +33 -2
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/tier_validation.py +41 -35
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/utils.py +8 -1
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/validate_parameters.py +46 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit.egg-info/PKG-INFO +17 -8
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit.egg-info/requires.txt +1 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/pyproject.toml +2 -1
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/setup.py +1 -1
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/tests/test_cli.py +7 -7
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/tests/test_dossier_generation.py +1 -1
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/tests/test_dossier_pages.py +1 -1
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/tests/test_tier_validation.py +57 -56
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/LICENSE +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/MANIFEST.in +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/assets/github-desktop_logo.png +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/assets/rges-pit_logo.png +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/__init__.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/__main__.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/commands/__init__.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/commands/dossier.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/commands/validation.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/dossier/__init__.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/dossier/dashboard.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/models/__init__.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/models/event.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/text_symbols.py +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit.egg-info/SOURCES.txt +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit.egg-info/dependency_links.txt +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit.egg-info/entry_points.txt +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit.egg-info/top_level.txt +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/setup.cfg +0 -0
- {microlens_submit-0.16.3 → microlens_submit-0.16.5}/tests/test_api.py +0 -0
|
@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.16.5] - 2026-02-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- Dossier assets are now copied into the dossier output and linked with URL-safe relative paths,
|
|
12
|
+
improving image loading in Roman Research Nexus and similar environments.
|
|
13
|
+
- Event data, plots, and posteriors now resolve reliably when paths include spaces or nested folders.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## [0.16.4] - 2026-01-14
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Added `remove-event`/`remove-solution` CLI commands and `remove_event`/`remove_solution` API helpers with a `--force` guard for hard deletes.
|
|
20
|
+
- Added `git_dir` metadata plus `set-git-dir` to capture Git info when the repo lives outside the submission directory.
|
|
21
|
+
- Added optional GPU fields in `hardware_info` (`gpu.model`, `gpu.count`, `gpu.memory_gb`) alongside platform/OS capture.
|
|
22
|
+
- Added non-Nexus hardware auto-fill using `psutil` for CPU and memory details.
|
|
23
|
+
- Added conda-forge recipe (`conda/recipe/meta.yaml`) to the version bump script (`scripts/bump_version`).
|
|
24
|
+
- Added sha256 update in `conda/recipe/meta.yaml` to the release workflow.
|
|
25
|
+
- Added a workflow release job to copy the local updated version on the conda-forge recipe to the feedstock fork (`AmberLee2427/microlens-submit-feedstock`) and send a PR, after PyPI release.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Updated tiers to `beginner`/`experienced`; event ID validation now uses inclusive ranges and 3-digit IDs for `2018-test`.
|
|
30
|
+
- CLI numeric parsing now accepts leading decimals like `.001`.
|
|
31
|
+
- Clarified quickstart/tutorial guidance around working directories and hardware info requirements.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- CSV import now skips blank rows to avoid NoneType parsing errors.
|
|
36
|
+
- Validation messaging now highlights missing bands when flux parameters are provided.
|
|
37
|
+
- Improved Windows notes editor fallback for better default editor selection.
|
|
38
|
+
|
|
39
|
+
|
|
8
40
|
## [0.16.3] - 2025-10-28
|
|
9
41
|
|
|
10
42
|
### Added
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
cff-version: 1.2.0
|
|
2
2
|
message: "If you use microlens-submit, please cite it as below."
|
|
3
3
|
title: "microlens-submit"
|
|
4
|
-
version: "0.16.
|
|
4
|
+
version: "0.16.5"
|
|
5
5
|
authors:
|
|
6
6
|
- family-names: Malpas
|
|
7
7
|
given-names: Amber
|
|
8
8
|
url: "https://github.com/AmberLee2427/microlens-submit"
|
|
9
|
-
doi: "10.5281/zenodo.
|
|
9
|
+
doi: "10.5281/zenodo.18246117"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microlens-submit
|
|
3
|
-
Version: 0.16.
|
|
3
|
+
Version: 0.16.5
|
|
4
4
|
Summary: A tool for managing and submitting microlensing solutions
|
|
5
5
|
Home-page: https://github.com/AmberLee2427/microlens-submit
|
|
6
6
|
Author: Amber Malpas
|
|
@@ -29,6 +29,7 @@ Requires-Dist: typer[all]>=0.9.0
|
|
|
29
29
|
Requires-Dist: rich>=13.0.0
|
|
30
30
|
Requires-Dist: pyyaml>=6.0
|
|
31
31
|
Requires-Dist: markdown>=3.4.0
|
|
32
|
+
Requires-Dist: psutil>=5.9.0
|
|
32
33
|
Requires-Dist: importlib_resources>=1.0.0; python_version < "3.9"
|
|
33
34
|
Provides-Extra: dev
|
|
34
35
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -94,18 +95,26 @@ The CLI is the recommended way to interact with your submission project.
|
|
|
94
95
|
|
|
95
96
|
You can pass ``--no-color`` to any command if your terminal does not support ANSI colors.
|
|
96
97
|
|
|
98
|
+
**Windows note:** If ``microlens-submit`` is not found after ``pip install``, your Python Scripts folder is likely missing from PATH.
|
|
99
|
+
Try ``py -m pip install microlens-submit`` and run ``py -m microlens_submit.cli --help``, or add the Scripts path shown by
|
|
100
|
+
``py -m pip show -f microlens-submit`` to PATH.
|
|
101
|
+
|
|
97
102
|
1. Initialize your project:
|
|
98
103
|
|
|
99
104
|
```bash
|
|
100
|
-
microlens-submit init --team-name "Planet Pounders" --tier "
|
|
101
|
-
|
|
105
|
+
microlens-submit init --team-name "Planet Pounders" --tier "experienced" ./my_submission
|
|
106
|
+
cd ./my_submission
|
|
102
107
|
```
|
|
103
108
|
|
|
104
|
-
|
|
109
|
+
If you prefer to initialize inside an existing folder, run `microlens-submit init` without a path after `cd` into it.
|
|
110
|
+
|
|
111
|
+
To pass validation, you need to have provided a `repo_url` and `hardware_info` to the project and have a git project initialized in your sumission-project directory. On Roman Nexus, you can use `microlens-submit nexus-init` to auto-populate hardware info.
|
|
105
112
|
|
|
106
113
|
```bash
|
|
107
114
|
microlens-submit set-repo-url <url> ./
|
|
108
115
|
microlens-submit set-hardware-info --cpu-details "intel i7 xxx" --ram-gb 32 ./
|
|
116
|
+
# if your git repo lives elsewhere:
|
|
117
|
+
microlens-submit set-git-dir /path/to/repo ./
|
|
109
118
|
```
|
|
110
119
|
2. Add a new solution to an event:
|
|
111
120
|
|
|
@@ -152,7 +161,7 @@ import microlens_submit
|
|
|
152
161
|
# Load or create the project
|
|
153
162
|
sub = microlens_submit.load(project_path="./my_challenge_submission")
|
|
154
163
|
sub.team_name = "Planet Pounders"
|
|
155
|
-
sub.tier = "
|
|
164
|
+
sub.tier = "experienced"
|
|
156
165
|
|
|
157
166
|
# Get an event and add a solution
|
|
158
167
|
evt = sub.get_event("ogle-2025-blg-0042")
|
|
@@ -201,15 +210,15 @@ import this file directly.
|
|
|
201
210
|
|
|
202
211
|
Bibtex:
|
|
203
212
|
```
|
|
204
|
-
@software{
|
|
213
|
+
@software{malpas_2025_18246117,
|
|
205
214
|
author = {Malpas, Amber},
|
|
206
215
|
title = {microlens-submit},
|
|
207
216
|
month = oct,
|
|
208
217
|
year = 2025,
|
|
209
218
|
publisher = {Zenodo},
|
|
210
219
|
version = {v0.16.3},
|
|
211
|
-
doi = {10.5281/zenodo.
|
|
212
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
220
|
+
doi = {10.5281/zenodo.18246117},
|
|
221
|
+
url = {https://doi.org/10.5281/zenodo.18246117},
|
|
213
222
|
}
|
|
214
223
|
```
|
|
215
224
|
|
|
@@ -45,18 +45,26 @@ The CLI is the recommended way to interact with your submission project.
|
|
|
45
45
|
|
|
46
46
|
You can pass ``--no-color`` to any command if your terminal does not support ANSI colors.
|
|
47
47
|
|
|
48
|
+
**Windows note:** If ``microlens-submit`` is not found after ``pip install``, your Python Scripts folder is likely missing from PATH.
|
|
49
|
+
Try ``py -m pip install microlens-submit`` and run ``py -m microlens_submit.cli --help``, or add the Scripts path shown by
|
|
50
|
+
``py -m pip show -f microlens-submit`` to PATH.
|
|
51
|
+
|
|
48
52
|
1. Initialize your project:
|
|
49
53
|
|
|
50
54
|
```bash
|
|
51
|
-
microlens-submit init --team-name "Planet Pounders" --tier "
|
|
52
|
-
|
|
55
|
+
microlens-submit init --team-name "Planet Pounders" --tier "experienced" ./my_submission
|
|
56
|
+
cd ./my_submission
|
|
53
57
|
```
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
If you prefer to initialize inside an existing folder, run `microlens-submit init` without a path after `cd` into it.
|
|
60
|
+
|
|
61
|
+
To pass validation, you need to have provided a `repo_url` and `hardware_info` to the project and have a git project initialized in your sumission-project directory. On Roman Nexus, you can use `microlens-submit nexus-init` to auto-populate hardware info.
|
|
56
62
|
|
|
57
63
|
```bash
|
|
58
64
|
microlens-submit set-repo-url <url> ./
|
|
59
65
|
microlens-submit set-hardware-info --cpu-details "intel i7 xxx" --ram-gb 32 ./
|
|
66
|
+
# if your git repo lives elsewhere:
|
|
67
|
+
microlens-submit set-git-dir /path/to/repo ./
|
|
60
68
|
```
|
|
61
69
|
2. Add a new solution to an event:
|
|
62
70
|
|
|
@@ -103,7 +111,7 @@ import microlens_submit
|
|
|
103
111
|
# Load or create the project
|
|
104
112
|
sub = microlens_submit.load(project_path="./my_challenge_submission")
|
|
105
113
|
sub.team_name = "Planet Pounders"
|
|
106
|
-
sub.tier = "
|
|
114
|
+
sub.tier = "experienced"
|
|
107
115
|
|
|
108
116
|
# Get an event and add a solution
|
|
109
117
|
evt = sub.get_event("ogle-2025-blg-0042")
|
|
@@ -152,15 +160,15 @@ import this file directly.
|
|
|
152
160
|
|
|
153
161
|
Bibtex:
|
|
154
162
|
```
|
|
155
|
-
@software{
|
|
163
|
+
@software{malpas_2025_18246117,
|
|
156
164
|
author = {Malpas, Amber},
|
|
157
165
|
title = {microlens-submit},
|
|
158
166
|
month = oct,
|
|
159
167
|
year = 2025,
|
|
160
168
|
publisher = {Zenodo},
|
|
161
169
|
version = {v0.16.3},
|
|
162
|
-
doi = {10.5281/zenodo.
|
|
163
|
-
url = {https://doi.org/10.5281/zenodo.
|
|
170
|
+
doi = {10.5281/zenodo.18246117},
|
|
171
|
+
url = {https://doi.org/10.5281/zenodo.18246117},
|
|
164
172
|
}
|
|
165
173
|
```
|
|
166
174
|
|
|
@@ -42,10 +42,13 @@ def export(
|
|
|
42
42
|
|
|
43
43
|
def remove_event(
|
|
44
44
|
event_id: str,
|
|
45
|
-
force: bool = typer.Option(False, "--force", help="
|
|
45
|
+
force: bool = typer.Option(False, "--force", help="Required to remove an event (prevents accidents)"),
|
|
46
46
|
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
47
47
|
) -> None:
|
|
48
|
-
"""Remove an entire event and all its solutions from the submission.
|
|
48
|
+
"""Remove an entire event and all its solutions from the submission.
|
|
49
|
+
|
|
50
|
+
This action is destructive and requires --force to proceed.
|
|
51
|
+
"""
|
|
49
52
|
submission = load(str(project_path))
|
|
50
53
|
|
|
51
54
|
if event_id not in submission.events:
|
|
@@ -57,13 +60,11 @@ def remove_event(
|
|
|
57
60
|
|
|
58
61
|
if not force:
|
|
59
62
|
typer.echo(
|
|
60
|
-
f"{symbol('warning')}
|
|
63
|
+
f"{symbol('warning')} Refusing to remove event '{event_id}' without --force "
|
|
64
|
+
f"({solution_count} solutions)."
|
|
61
65
|
)
|
|
62
|
-
typer.echo("
|
|
63
|
-
|
|
64
|
-
if not confirm:
|
|
65
|
-
typer.echo(f"{symbol('error')} Operation cancelled")
|
|
66
|
-
raise typer.Exit(0)
|
|
66
|
+
typer.echo(f"{symbol('hint')} Consider deactivating solutions instead, or re-run with --force to proceed.")
|
|
67
|
+
raise typer.Exit(0)
|
|
67
68
|
|
|
68
69
|
try:
|
|
69
70
|
removed = submission.remove_event(event_id, force=force)
|
|
@@ -94,11 +95,33 @@ def set_repo_url(
|
|
|
94
95
|
)
|
|
95
96
|
|
|
96
97
|
|
|
98
|
+
def set_git_dir(
|
|
99
|
+
git_dir: Path = typer.Argument(..., help="Path to the git working tree"),
|
|
100
|
+
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Set or update the git working tree path in the submission metadata."""
|
|
103
|
+
sub = load(str(project_path))
|
|
104
|
+
git_dir_path = git_dir.expanduser().resolve()
|
|
105
|
+
if not git_dir_path.exists():
|
|
106
|
+
raise typer.BadParameter(f"git_dir does not exist: {git_dir_path}")
|
|
107
|
+
sub.git_dir = str(git_dir_path)
|
|
108
|
+
sub.save()
|
|
109
|
+
console.print(
|
|
110
|
+
Panel(
|
|
111
|
+
f"Set git_dir to {git_dir_path} in {project_path}/submission.json",
|
|
112
|
+
style="bold green",
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
97
117
|
def set_hardware_info(
|
|
98
118
|
cpu: Optional[str] = typer.Option(None, "--cpu", help="CPU model/description"),
|
|
99
119
|
cpu_details: Optional[str] = typer.Option(None, "--cpu-details", help="Detailed CPU information"),
|
|
100
120
|
memory_gb: Optional[float] = typer.Option(None, "--memory-gb", help="Memory in GB"),
|
|
101
121
|
ram_gb: Optional[float] = typer.Option(None, "--ram-gb", help="RAM in GB (alternative to --memory-gb)"),
|
|
122
|
+
gpu: Optional[str] = typer.Option(None, "--gpu", help="GPU model/description"),
|
|
123
|
+
gpu_count: Optional[int] = typer.Option(None, "--gpu-count", help="Number of GPUs"),
|
|
124
|
+
gpu_memory_gb: Optional[float] = typer.Option(None, "--gpu-memory-gb", help="GPU memory per device in GB"),
|
|
102
125
|
platform: Optional[str] = typer.Option(
|
|
103
126
|
None,
|
|
104
127
|
"--platform",
|
|
@@ -153,6 +176,24 @@ def set_hardware_info(
|
|
|
153
176
|
changes.append(f"Set nexus_image: {nexus_image}")
|
|
154
177
|
sub.hardware_info["nexus_image"] = nexus_image
|
|
155
178
|
|
|
179
|
+
if any(value is not None for value in (gpu, gpu_count, gpu_memory_gb)):
|
|
180
|
+
gpu_info = sub.hardware_info.get("gpu")
|
|
181
|
+
if not isinstance(gpu_info, dict):
|
|
182
|
+
gpu_info = {}
|
|
183
|
+
if gpu is not None:
|
|
184
|
+
if gpu_info.get("model") != gpu:
|
|
185
|
+
changes.append(f"Set gpu.model: {gpu}")
|
|
186
|
+
gpu_info["model"] = gpu
|
|
187
|
+
if gpu_count is not None:
|
|
188
|
+
if gpu_info.get("count") != gpu_count:
|
|
189
|
+
changes.append(f"Set gpu.count: {gpu_count}")
|
|
190
|
+
gpu_info["count"] = gpu_count
|
|
191
|
+
if gpu_memory_gb is not None:
|
|
192
|
+
if gpu_info.get("memory_gb") != gpu_memory_gb:
|
|
193
|
+
changes.append(f"Set gpu.memory_gb: {gpu_memory_gb}")
|
|
194
|
+
gpu_info["memory_gb"] = gpu_memory_gb
|
|
195
|
+
sub.hardware_info["gpu"] = gpu_info
|
|
196
|
+
|
|
156
197
|
# Show dry run results
|
|
157
198
|
if dry_run:
|
|
158
199
|
if changes:
|
|
@@ -17,6 +17,7 @@ def init(
|
|
|
17
17
|
team_name: str = typer.Option(..., help="Team name"),
|
|
18
18
|
tier: str = typer.Option(..., help="Challenge tier"),
|
|
19
19
|
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
20
|
+
show_warnings: bool = True,
|
|
20
21
|
) -> None:
|
|
21
22
|
"""Create a new submission project in the specified directory.
|
|
22
23
|
|
|
@@ -29,7 +30,7 @@ def init(
|
|
|
29
30
|
|
|
30
31
|
Args:
|
|
31
32
|
team_name: Name of the participating team (e.g., "Team Alpha").
|
|
32
|
-
tier: Challenge tier level (e.g., "
|
|
33
|
+
tier: Challenge tier level (e.g., "beginner", "experienced").
|
|
33
34
|
project_path: Directory where the project will be created.
|
|
34
35
|
Defaults to current directory if not specified.
|
|
35
36
|
|
|
@@ -38,10 +39,10 @@ def init(
|
|
|
38
39
|
|
|
39
40
|
Example:
|
|
40
41
|
# Create project in current directory
|
|
41
|
-
microlens-submit init --team-name "Team Alpha" --tier "
|
|
42
|
+
microlens-submit init --team-name "Team Alpha" --tier "experienced"
|
|
42
43
|
|
|
43
44
|
# Create project in specific directory
|
|
44
|
-
microlens-submit init --team-name "Team Beta" --tier "
|
|
45
|
+
microlens-submit init --team-name "Team Beta" --tier "beginner" ./my_submission
|
|
45
46
|
|
|
46
47
|
# Project structure created:
|
|
47
48
|
# ./my_submission/
|
|
@@ -100,12 +101,15 @@ def init(
|
|
|
100
101
|
)
|
|
101
102
|
|
|
102
103
|
# Run warnings-only validation
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
if show_warnings:
|
|
105
|
+
warnings = sub.run_validation_warnings()
|
|
106
|
+
if warnings:
|
|
107
|
+
console.print(f"[yellow]{symbol('warning')} Project initialized with warnings:[/yellow]")
|
|
108
|
+
for warning in warnings:
|
|
109
|
+
console.print(f" [yellow]• {warning}[/yellow]")
|
|
110
|
+
console.print(
|
|
111
|
+
f"[yellow]{symbol('hint')} These warnings will become errors when saving or exporting.[/yellow]"
|
|
112
|
+
)
|
|
109
113
|
|
|
110
114
|
# Try to save, but don't fail if there are validation errors
|
|
111
115
|
try:
|
|
@@ -134,13 +138,13 @@ def nexus_init(
|
|
|
134
138
|
|
|
135
139
|
Args:
|
|
136
140
|
team_name: Name of the participating team (e.g., "Team Alpha").
|
|
137
|
-
tier: Challenge tier level (e.g., "
|
|
141
|
+
tier: Challenge tier level (e.g., "beginner", "experienced").
|
|
138
142
|
project_path: Directory where the project will be created.
|
|
139
143
|
Defaults to current directory if not specified.
|
|
140
144
|
|
|
141
145
|
Example:
|
|
142
146
|
# Initialize project with Nexus platform info
|
|
143
|
-
microlens-submit nexus-init --team-name "Team Alpha" --tier "
|
|
147
|
+
microlens-submit nexus-init --team-name "Team Alpha" --tier "experienced" ./project
|
|
144
148
|
|
|
145
149
|
# This will automatically detect:
|
|
146
150
|
# - CPU model from /proc/cpuinfo
|
|
@@ -152,7 +156,7 @@ def nexus_init(
|
|
|
152
156
|
environment. It will silently skip any environment information that
|
|
153
157
|
cannot be detected (e.g., if running outside of Nexus).
|
|
154
158
|
"""
|
|
155
|
-
init(team_name=team_name, tier=tier, project_path=project_path)
|
|
159
|
+
init(team_name=team_name, tier=tier, project_path=project_path, show_warnings=False)
|
|
156
160
|
sub = load(str(project_path))
|
|
157
161
|
sub.autofill_nexus_info()
|
|
158
162
|
|
{microlens_submit-0.16.3 → microlens_submit-0.16.5}/microlens_submit/cli/commands/solutions.py
RENAMED
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
5
10
|
from pathlib import Path
|
|
6
|
-
from typing import Dict, List, Optional, Tuple
|
|
11
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
12
|
|
|
8
13
|
import typer
|
|
9
14
|
from rich.console import Console
|
|
@@ -16,6 +21,38 @@ from microlens_submit.utils import import_solutions_from_csv, load
|
|
|
16
21
|
console = Console()
|
|
17
22
|
|
|
18
23
|
|
|
24
|
+
_NUMERIC_RE = re.compile(r"^[+-]?((\\d+(\\.\\d*)?)|(\\.\\d+))([eE][+-]?\\d+)?$")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_cli_value(value: str) -> Any:
|
|
28
|
+
"""Parse a CLI value using JSON, with a numeric fallback for .001-style input."""
|
|
29
|
+
try:
|
|
30
|
+
return json.loads(value)
|
|
31
|
+
except json.JSONDecodeError:
|
|
32
|
+
if _NUMERIC_RE.match(value.strip()):
|
|
33
|
+
try:
|
|
34
|
+
if re.match(r"^[+-]?\\d+$", value.strip()):
|
|
35
|
+
return int(value)
|
|
36
|
+
return float(value)
|
|
37
|
+
except ValueError:
|
|
38
|
+
pass
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_editor(editor_cmd: str, notes_file: Path) -> bool:
|
|
43
|
+
parts = shlex.split(editor_cmd)
|
|
44
|
+
if not parts:
|
|
45
|
+
return False
|
|
46
|
+
if os.name == "nt" and parts[0].lower() in ("code", "code.exe"):
|
|
47
|
+
if "--wait" not in parts and "-w" not in parts:
|
|
48
|
+
parts.append("--wait")
|
|
49
|
+
try:
|
|
50
|
+
subprocess.run(parts + [str(notes_file)], check=False)
|
|
51
|
+
return True
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
19
56
|
def _parse_pairs(pairs: Optional[List[str]]) -> Optional[Dict]:
|
|
20
57
|
"""Convert CLI key=value options into a dictionary."""
|
|
21
58
|
if not pairs:
|
|
@@ -25,10 +62,7 @@ def _parse_pairs(pairs: Optional[List[str]]) -> Optional[Dict]:
|
|
|
25
62
|
if "=" not in item:
|
|
26
63
|
raise typer.BadParameter(f"Invalid format: {item}")
|
|
27
64
|
key, value = item.split("=", 1)
|
|
28
|
-
|
|
29
|
-
out[key] = json.loads(value)
|
|
30
|
-
except json.JSONDecodeError:
|
|
31
|
-
out[key] = value
|
|
65
|
+
out[key] = _parse_cli_value(value)
|
|
32
66
|
return out
|
|
33
67
|
|
|
34
68
|
|
|
@@ -219,10 +253,7 @@ def add_solution(
|
|
|
219
253
|
if "=" not in p:
|
|
220
254
|
raise typer.BadParameter(f"Invalid parameter format: {p}")
|
|
221
255
|
key, value = p.split("=", 1)
|
|
222
|
-
|
|
223
|
-
params[key] = json.loads(value)
|
|
224
|
-
except json.JSONDecodeError:
|
|
225
|
-
params[key] = value
|
|
256
|
+
params[key] = _parse_cli_value(value)
|
|
226
257
|
allowed_model_types = [
|
|
227
258
|
"1S1L",
|
|
228
259
|
"1S2L",
|
|
@@ -254,7 +285,7 @@ def add_solution(
|
|
|
254
285
|
sol.relative_probability = relative_probability
|
|
255
286
|
sol.n_data_points = n_data_points
|
|
256
287
|
if cpu_hours is not None or wall_time_hours is not None:
|
|
257
|
-
sol.set_compute_info(cpu_hours=cpu_hours, wall_time_hours=wall_time_hours)
|
|
288
|
+
sol.set_compute_info(cpu_hours=cpu_hours, wall_time_hours=wall_time_hours, git_dir=sub.git_dir)
|
|
258
289
|
sol.lightcurve_plot_path = str(lightcurve_plot_path) if lightcurve_plot_path else None
|
|
259
290
|
sol.lens_plane_plot_path = str(lens_plane_plot_path) if lens_plane_plot_path else None
|
|
260
291
|
# Handle notes file logic
|
|
@@ -368,10 +399,13 @@ def activate(
|
|
|
368
399
|
|
|
369
400
|
def remove_solution(
|
|
370
401
|
solution_id: str,
|
|
371
|
-
force: bool = typer.Option(False, "--force", help="
|
|
402
|
+
force: bool = typer.Option(False, "--force", help="Required to remove a solution (prevents accidents)"),
|
|
372
403
|
project_path: Path = typer.Argument(Path("."), help="Project directory"),
|
|
373
404
|
) -> None:
|
|
374
|
-
"""Completely remove a solution from the submission.
|
|
405
|
+
"""Completely remove a solution from the submission.
|
|
406
|
+
|
|
407
|
+
This action is destructive and requires --force to proceed.
|
|
408
|
+
"""
|
|
375
409
|
submission = load(project_path)
|
|
376
410
|
|
|
377
411
|
# Find the solution across all events
|
|
@@ -387,6 +421,16 @@ def remove_solution(
|
|
|
387
421
|
console.print(f"[red]Error: Solution {solution_id} not found[/red]")
|
|
388
422
|
raise typer.Exit(1)
|
|
389
423
|
|
|
424
|
+
if not force:
|
|
425
|
+
console.print(
|
|
426
|
+
f"[yellow]{symbol('warning')} Refusing to remove solution {solution_id[:8]}... without --force.[/yellow]"
|
|
427
|
+
)
|
|
428
|
+
console.print(
|
|
429
|
+
f"[blue]{symbol('hint')} Consider using deactivate to keep the solution, "
|
|
430
|
+
"or re-run with --force to proceed.[/blue]"
|
|
431
|
+
)
|
|
432
|
+
raise typer.Exit(0)
|
|
433
|
+
|
|
390
434
|
try:
|
|
391
435
|
removed = submission.events[event_id].remove_solution(solution_id, force=force)
|
|
392
436
|
if removed:
|
|
@@ -561,16 +605,14 @@ def edit_solution(
|
|
|
561
605
|
target_solution.set_compute_info(
|
|
562
606
|
cpu_hours=cpu_hours if cpu_hours is not None else old_cpu,
|
|
563
607
|
wall_time_hours=(wall_time_hours if wall_time_hours is not None else old_wall),
|
|
608
|
+
git_dir=sub.git_dir,
|
|
564
609
|
)
|
|
565
610
|
if param:
|
|
566
611
|
for p in param:
|
|
567
612
|
if "=" not in p:
|
|
568
613
|
raise typer.BadParameter(f"Invalid parameter format: {p}")
|
|
569
614
|
key, value = p.split("=", 1)
|
|
570
|
-
|
|
571
|
-
new_value = json.loads(value)
|
|
572
|
-
except json.JSONDecodeError:
|
|
573
|
-
new_value = value
|
|
615
|
+
new_value = _parse_cli_value(value)
|
|
574
616
|
old_value = target_solution.parameters.get(key)
|
|
575
617
|
if old_value != new_value:
|
|
576
618
|
changes.append(f"Update parameter {key}: {old_value} {arrow} {new_value}")
|
|
@@ -582,10 +624,7 @@ def edit_solution(
|
|
|
582
624
|
if "=" not in p:
|
|
583
625
|
raise typer.BadParameter(f"Invalid uncertainty format: {p}")
|
|
584
626
|
key, value = p.split("=", 1)
|
|
585
|
-
|
|
586
|
-
new_value = json.loads(value)
|
|
587
|
-
except json.JSONDecodeError:
|
|
588
|
-
new_value = value
|
|
627
|
+
new_value = _parse_cli_value(value)
|
|
589
628
|
old_value = target_solution.parameter_uncertainties.get(key)
|
|
590
629
|
if old_value != new_value:
|
|
591
630
|
changes.append(f"Update uncertainty {key}: {old_value} {arrow} {new_value}")
|
|
@@ -636,21 +675,35 @@ def edit_notes(
|
|
|
636
675
|
notes_file.parent.mkdir(parents=True, exist_ok=True)
|
|
637
676
|
if not notes_file.exists():
|
|
638
677
|
notes_file.write_text("", encoding="utf-8")
|
|
639
|
-
editor = os.environ.get("EDITOR"
|
|
640
|
-
if editor:
|
|
641
|
-
|
|
678
|
+
editor = os.environ.get("EDITOR") or os.environ.get("VISUAL")
|
|
679
|
+
if editor and _run_editor(editor, notes_file):
|
|
680
|
+
return
|
|
681
|
+
fallbacks = ["nano", "vi", "vim", "code"]
|
|
682
|
+
if os.name == "nt":
|
|
683
|
+
fallbacks = ["code", "notepad", "notepad.exe"]
|
|
684
|
+
for fallback in fallbacks:
|
|
685
|
+
if shutil.which(fallback):
|
|
686
|
+
if _run_editor(fallback, notes_file):
|
|
687
|
+
return
|
|
688
|
+
if os.name == "nt":
|
|
689
|
+
try:
|
|
690
|
+
os.startfile(notes_file) # type: ignore[attr-defined]
|
|
691
|
+
return
|
|
692
|
+
except OSError:
|
|
693
|
+
pass
|
|
694
|
+
elif sys.platform == "darwin":
|
|
695
|
+
if shutil.which("open"):
|
|
696
|
+
subprocess.run(["open", "-W", str(notes_file)], check=False)
|
|
697
|
+
return
|
|
642
698
|
else:
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
style="bold red",
|
|
652
|
-
)
|
|
653
|
-
raise typer.Exit(code=1)
|
|
699
|
+
if shutil.which("xdg-open"):
|
|
700
|
+
subprocess.run(["xdg-open", str(notes_file)], check=False)
|
|
701
|
+
return
|
|
702
|
+
console.print(
|
|
703
|
+
f"Could not find an editor to open {notes_file}",
|
|
704
|
+
style="bold red",
|
|
705
|
+
)
|
|
706
|
+
raise typer.Exit(code=1)
|
|
654
707
|
return
|
|
655
708
|
console.print(f"Solution {solution_id} not found", style="bold red")
|
|
656
709
|
raise typer.Exit(code=1)
|
|
@@ -17,7 +17,7 @@ and scripted usage patterns.
|
|
|
17
17
|
|
|
18
18
|
**Example Workflow:**
|
|
19
19
|
# Initialize a new project
|
|
20
|
-
microlens-submit init --team-name "Team Alpha" --tier "
|
|
20
|
+
microlens-submit init --team-name "Team Alpha" --tier "experienced" ./my_project
|
|
21
21
|
|
|
22
22
|
# Add a solution
|
|
23
23
|
microlens-submit add-solution EVENT001 1S1L ./my_project \
|
|
@@ -117,4 +117,5 @@ app.command("generate-dossier")(dossier.generate_dossier)
|
|
|
117
117
|
app.command("export")(export.export)
|
|
118
118
|
app.command("remove-event")(export.remove_event)
|
|
119
119
|
app.command("set-repo-url")(export.set_repo_url)
|
|
120
|
+
app.command("set-git-dir")(export.set_git_dir)
|
|
120
121
|
app.command("set-hardware-info")(export.set_hardware_info)
|
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from .. import __version__
|
|
13
13
|
from ..models import Event, Submission
|
|
14
14
|
from .solution_page import generate_solution_page
|
|
15
|
+
from .utils import resolve_dossier_asset_path
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def generate_event_page(event: Event, submission: Submission, output_dir: Path) -> None:
|
|
@@ -50,7 +51,17 @@ def generate_event_page(event: Event, submission: Submission, output_dir: Path)
|
|
|
50
51
|
to individual solution pages.
|
|
51
52
|
"""
|
|
52
53
|
# Prepare output directory (already created)
|
|
53
|
-
|
|
54
|
+
project_root = Path(submission.project_path)
|
|
55
|
+
event_data_link = ""
|
|
56
|
+
if hasattr(event, "event_data_path") and event.event_data_path:
|
|
57
|
+
event_data_link = resolve_dossier_asset_path(
|
|
58
|
+
event.event_data_path,
|
|
59
|
+
project_root,
|
|
60
|
+
output_dir,
|
|
61
|
+
subdir="event-data",
|
|
62
|
+
prefix=f"{event.event_id}_event_data",
|
|
63
|
+
)
|
|
64
|
+
html = _generate_event_page_content(event, submission, event_data_link=event_data_link)
|
|
54
65
|
with (output_dir / f"{event.event_id}.html").open("w", encoding="utf-8") as f:
|
|
55
66
|
f.write(html)
|
|
56
67
|
|
|
@@ -59,7 +70,12 @@ def generate_event_page(event: Event, submission: Submission, output_dir: Path)
|
|
|
59
70
|
generate_solution_page(sol, event, submission, output_dir)
|
|
60
71
|
|
|
61
72
|
|
|
62
|
-
def _generate_event_page_content(
|
|
73
|
+
def _generate_event_page_content(
|
|
74
|
+
event: Event,
|
|
75
|
+
submission: Submission,
|
|
76
|
+
*,
|
|
77
|
+
event_data_link: str = "",
|
|
78
|
+
) -> str:
|
|
63
79
|
"""Generate the HTML content for an event dossier page.
|
|
64
80
|
|
|
65
81
|
Creates the complete HTML content for a single event page, including
|
|
@@ -163,10 +179,10 @@ def _generate_event_page_content(event: Event, submission: Submission) -> str:
|
|
|
163
179
|
)
|
|
164
180
|
# Optional raw data link
|
|
165
181
|
raw_data_html = ""
|
|
166
|
-
if
|
|
182
|
+
if event_data_link:
|
|
167
183
|
raw_data_html = (
|
|
168
184
|
f'<p class="text-rtd-text">Raw Event Data: '
|
|
169
|
-
f'<a href="{
|
|
185
|
+
f'<a href="{event_data_link}" '
|
|
170
186
|
f'class="text-rtd-accent hover:underline">Download Data</a></p>'
|
|
171
187
|
)
|
|
172
188
|
# HTML content
|