microlens-submit 0.16.0__tar.gz → 0.16.2__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.
Files changed (45) hide show
  1. {microlens_submit-0.16.0/microlens_submit.egg-info → microlens_submit-0.16.2}/PKG-INFO +53 -9
  2. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/README.md +46 -7
  3. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/__init__.py +1 -1
  4. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/commands/export.py +9 -6
  5. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/commands/init.py +13 -8
  6. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/commands/solutions.py +26 -21
  7. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/commands/validation.py +11 -7
  8. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/dossier/dashboard.py +6 -2
  9. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/dossier/event_page.py +2 -1
  10. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/dossier/solution_page.py +2 -1
  11. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/error_messages.py +6 -4
  12. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/models/event.py +10 -7
  13. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/models/submission.py +23 -22
  14. microlens_submit-0.16.2/microlens_submit/text_symbols.py +75 -0
  15. {microlens_submit-0.16.0 → microlens_submit-0.16.2/microlens_submit.egg-info}/PKG-INFO +53 -9
  16. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit.egg-info/SOURCES.txt +1 -0
  17. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit.egg-info/requires.txt +1 -0
  18. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/pyproject.toml +4 -3
  19. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/setup.py +2 -2
  20. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/tests/test_cli.py +20 -15
  21. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/LICENSE +0 -0
  22. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/MANIFEST.in +0 -0
  23. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/assets/github-desktop_logo.png +0 -0
  24. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/assets/rges-pit_logo.png +0 -0
  25. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/__init__.py +0 -0
  26. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/__main__.py +0 -0
  27. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/commands/__init__.py +0 -0
  28. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/commands/dossier.py +0 -0
  29. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/cli/main.py +0 -0
  30. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/dossier/__init__.py +0 -0
  31. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/dossier/full_report.py +0 -0
  32. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/dossier/utils.py +0 -0
  33. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/models/__init__.py +0 -0
  34. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/models/solution.py +0 -0
  35. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/tier_validation.py +0 -0
  36. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/utils.py +0 -0
  37. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit/validate_parameters.py +0 -0
  38. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit.egg-info/dependency_links.txt +0 -0
  39. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit.egg-info/entry_points.txt +0 -0
  40. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/microlens_submit.egg-info/top_level.txt +0 -0
  41. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/setup.cfg +0 -0
  42. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/tests/test_api.py +0 -0
  43. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/tests/test_dossier_generation.py +0 -0
  44. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/tests/test_dossier_pages.py +0 -0
  45. {microlens_submit-0.16.0 → microlens_submit-0.16.2}/tests/test_tier_validation.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: microlens-submit
3
- Version: 0.16.0
3
+ Version: 0.16.2
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
@@ -41,6 +41,11 @@ Requires-Dist: isort; extra == "dev"
41
41
  Requires-Dist: sphinx; extra == "dev"
42
42
  Requires-Dist: sphinx_rtd_theme; extra == "dev"
43
43
  Requires-Dist: importlib_resources; extra == "dev"
44
+ Requires-Dist: python-dotenv; extra == "dev"
45
+ Dynamic: author
46
+ Dynamic: home-page
47
+ Dynamic: license-file
48
+ Dynamic: requires-python
44
49
 
45
50
  <p align="center">
46
51
  <a href="https://github.com/AmberLee2427/microlens-submit">
@@ -52,11 +57,7 @@ Requires-Dist: importlib_resources; extra == "dev"
52
57
 
53
58
  *A stateful submission toolkit for the RGES-PIT Microlensing Data Challenge.*
54
59
 
55
- [![PyPI - v0.16.0](https://img.shields.io/pypi/v/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
56
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
57
- [![PyPI - License](https://img.shields.io/pypi/l/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
58
- [![CI](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml/badge.svg)](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml)
59
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
60
+ [![PyPI - v0.16.0](https://img.shields.io/pypi/v/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)[![Read the Docs](https://readthedocs.org/projects/microlens-submit/badge/?version=latest)](https://microlens-submit.readthedocs.io/en/latest/?badge=latest)[![CI](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml/badge.svg)](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml)[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
60
61
 
61
62
  <br>
62
63
 
@@ -93,8 +94,21 @@ The CLI is the recommended way to interact with your submission project.
93
94
 
94
95
  You can pass ``--no-color`` to any command if your terminal does not support ANSI colors.
95
96
 
96
- 1. Initialize your project: `microlens-submit init --team-name "Planet Pounders" --tier "advanced"`
97
+ 1. Initialize your project:
98
+
99
+ ```bash
100
+ microlens-submit init --team-name "Planet Pounders" --tier "advanced"
101
+ # if a project directory was provided to `init`, you should now `cd` into that project
102
+ ```
103
+
104
+ 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.
105
+
106
+ ```bash
107
+ microlens-submit set-repo-url <url> ./
108
+ microlens-submit set-hardware-info --cpu-details "intel i7 xxx" --ram-gb 32 ./
109
+ ```
97
110
  2. Add a new solution to an event:
111
+
98
112
  ```bash
99
113
  microlens-submit add-solution ogle-2025-blg-0042 1S2L \
100
114
  --param t0=555.5 \
@@ -102,18 +116,28 @@ You can pass ``--no-color`` to any command if your terminal does not support ANS
102
116
  --param tE=25.0 \
103
117
  --notes "This is a great fit!"
104
118
  ```
119
+
105
120
  Model types must be one of `1S1L`, `1S2L`, `2S1L`, `2S2L`, `1S3L`, `2S3L`, or `other`.
106
121
  This will create a new solution and print its unique `solution_id`.
122
+
107
123
  You can run the same command with `--dry-run` first to verify the
108
124
  parsed input without saving anything.
125
+
109
126
  3. **Bulk import multiple solutions from a CSV file:**
127
+
110
128
  ```bash
111
129
  microlens-submit import-solutions tests/data/test_import.csv --dry-run
112
130
  ```
113
- See the file `tests/data/test_import.csv` for a comprehensive example covering all features and edge cases. You can use this file as a template for your own imports.
131
+
132
+ See the file `tests/data/test_import.csv` for a comprehensive example covering all features and edge cases.
133
+ You can use this file as a template for your own imports.
134
+
114
135
  4. Deactivate a solution that didn't work out: `microlens-submit deactivate <solution_id>`
136
+
115
137
  5. List all solutions for an event: `microlens-submit list-solutions ogle-2025-blg-0042`
138
+
116
139
  6. Validate solutions and check for issues: `microlens-submit validate-solution <solution_id>`
140
+
117
141
  7. Export your final submission: `microlens-submit export final_submission.zip`
118
142
 
119
143
  **Note:** When you add a solution, it's automatically validated and any warnings are displayed. Use `--dry-run` to check validation without saving.
@@ -174,3 +198,23 @@ A comprehensive test CSV file is provided at `tests/data/test_import.csv`. This
174
198
  If you use **microlens-submit** in your research, please cite the project using
175
199
  the metadata provided in the `CITATION.cff` file. Most reference managers can
176
200
  import this file directly.
201
+
202
+ Bibtex:
203
+ ```
204
+ @software{malpas_2025_17460500,
205
+ author = {Malpas, Amber},
206
+ title = {microlens-submit},
207
+ month = oct,
208
+ year = 2025,
209
+ publisher = {Zenodo},
210
+ version = {v0.16.1},
211
+ doi = {10.5281/zenodo.17460500},
212
+ url = {https://doi.org/10.5281/zenodo.17460500},
213
+ }
214
+ ```
215
+
216
+ Cite without version:
217
+ Malpas, A. (2025). microlens-submit. Zenodo. https://doi.org/10.5281/zenodo.17459752
218
+
219
+ Cite current version:
220
+ Malpas, A. (2025). microlens-submit (v0.16.1). Zenodo. https://doi.org/10.5281/zenodo.17459753
@@ -8,11 +8,7 @@
8
8
 
9
9
  *A stateful submission toolkit for the RGES-PIT Microlensing Data Challenge.*
10
10
 
11
- [![PyPI - v0.16.0](https://img.shields.io/pypi/v/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
12
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
13
- [![PyPI - License](https://img.shields.io/pypi/l/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
14
- [![CI](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml/badge.svg)](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml)
15
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+ [![PyPI - v0.16.0](https://img.shields.io/pypi/v/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)[![Read the Docs](https://readthedocs.org/projects/microlens-submit/badge/?version=latest)](https://microlens-submit.readthedocs.io/en/latest/?badge=latest)[![CI](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml/badge.svg)](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml)[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
16
12
 
17
13
  <br>
18
14
 
@@ -49,8 +45,21 @@ The CLI is the recommended way to interact with your submission project.
49
45
 
50
46
  You can pass ``--no-color`` to any command if your terminal does not support ANSI colors.
51
47
 
52
- 1. Initialize your project: `microlens-submit init --team-name "Planet Pounders" --tier "advanced"`
48
+ 1. Initialize your project:
49
+
50
+ ```bash
51
+ microlens-submit init --team-name "Planet Pounders" --tier "advanced"
52
+ # if a project directory was provided to `init`, you should now `cd` into that project
53
+ ```
54
+
55
+ 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.
56
+
57
+ ```bash
58
+ microlens-submit set-repo-url <url> ./
59
+ microlens-submit set-hardware-info --cpu-details "intel i7 xxx" --ram-gb 32 ./
60
+ ```
53
61
  2. Add a new solution to an event:
62
+
54
63
  ```bash
55
64
  microlens-submit add-solution ogle-2025-blg-0042 1S2L \
56
65
  --param t0=555.5 \
@@ -58,18 +67,28 @@ You can pass ``--no-color`` to any command if your terminal does not support ANS
58
67
  --param tE=25.0 \
59
68
  --notes "This is a great fit!"
60
69
  ```
70
+
61
71
  Model types must be one of `1S1L`, `1S2L`, `2S1L`, `2S2L`, `1S3L`, `2S3L`, or `other`.
62
72
  This will create a new solution and print its unique `solution_id`.
73
+
63
74
  You can run the same command with `--dry-run` first to verify the
64
75
  parsed input without saving anything.
76
+
65
77
  3. **Bulk import multiple solutions from a CSV file:**
78
+
66
79
  ```bash
67
80
  microlens-submit import-solutions tests/data/test_import.csv --dry-run
68
81
  ```
69
- See the file `tests/data/test_import.csv` for a comprehensive example covering all features and edge cases. You can use this file as a template for your own imports.
82
+
83
+ See the file `tests/data/test_import.csv` for a comprehensive example covering all features and edge cases.
84
+ You can use this file as a template for your own imports.
85
+
70
86
  4. Deactivate a solution that didn't work out: `microlens-submit deactivate <solution_id>`
87
+
71
88
  5. List all solutions for an event: `microlens-submit list-solutions ogle-2025-blg-0042`
89
+
72
90
  6. Validate solutions and check for issues: `microlens-submit validate-solution <solution_id>`
91
+
73
92
  7. Export your final submission: `microlens-submit export final_submission.zip`
74
93
 
75
94
  **Note:** When you add a solution, it's automatically validated and any warnings are displayed. Use `--dry-run` to check validation without saving.
@@ -130,3 +149,23 @@ A comprehensive test CSV file is provided at `tests/data/test_import.csv`. This
130
149
  If you use **microlens-submit** in your research, please cite the project using
131
150
  the metadata provided in the `CITATION.cff` file. Most reference managers can
132
151
  import this file directly.
152
+
153
+ Bibtex:
154
+ ```
155
+ @software{malpas_2025_17460500,
156
+ author = {Malpas, Amber},
157
+ title = {microlens-submit},
158
+ month = oct,
159
+ year = 2025,
160
+ publisher = {Zenodo},
161
+ version = {v0.16.1},
162
+ doi = {10.5281/zenodo.17460500},
163
+ url = {https://doi.org/10.5281/zenodo.17460500},
164
+ }
165
+ ```
166
+
167
+ Cite without version:
168
+ Malpas, A. (2025). microlens-submit. Zenodo. https://doi.org/10.5281/zenodo.17459752
169
+
170
+ Cite current version:
171
+ Malpas, A. (2025). microlens-submit (v0.16.1). Zenodo. https://doi.org/10.5281/zenodo.17459753
@@ -5,7 +5,7 @@ validate, and export a challenge submission using either the Python API or
5
5
  the command line interface.
6
6
  """
7
7
 
8
- __version__ = "0.16.0"
8
+ __version__ = "0.16.2"
9
9
 
10
10
  from .models import Event, Solution, Submission
11
11
  from .utils import load
@@ -7,6 +7,7 @@ import typer
7
7
  from rich.console import Console
8
8
  from rich.panel import Panel
9
9
 
10
+ from microlens_submit.text_symbols import symbol
10
11
  from microlens_submit.utils import load
11
12
 
12
13
  console = Console()
@@ -48,30 +49,32 @@ def remove_event(
48
49
  submission = load(str(project_path))
49
50
 
50
51
  if event_id not in submission.events:
51
- typer.echo(f" Event '{event_id}' not found in submission")
52
+ typer.echo(f"{symbol('error')} Event '{event_id}' not found in submission")
52
53
  raise typer.Exit(1)
53
54
 
54
55
  event = submission.events[event_id]
55
56
  solution_count = len(event.solutions)
56
57
 
57
58
  if not force:
58
- typer.echo(f"⚠️ This will permanently remove event '{event_id}' " f"and all {solution_count} solutions.")
59
+ typer.echo(
60
+ f"{symbol('warning')} This will permanently remove event '{event_id}' and all {solution_count} solutions."
61
+ )
59
62
  typer.echo(" This action cannot be undone.")
60
63
  confirm = typer.confirm("Are you sure you want to continue?")
61
64
  if not confirm:
62
- typer.echo(" Operation cancelled")
65
+ typer.echo(f"{symbol('error')} Operation cancelled")
63
66
  raise typer.Exit(0)
64
67
 
65
68
  try:
66
69
  removed = submission.remove_event(event_id, force=force)
67
70
  if removed:
68
- typer.echo(f" Removed event '{event_id}' and all {solution_count} solutions")
71
+ typer.echo(f"{symbol('check')} Removed event '{event_id}' and all {solution_count} solutions")
69
72
  submission.save()
70
73
  else:
71
- typer.echo(f" Failed to remove event '{event_id}'")
74
+ typer.echo(f"{symbol('error')} Failed to remove event '{event_id}'")
72
75
  raise typer.Exit(1)
73
76
  except ValueError as e:
74
- typer.echo(f" Cannot remove event: {e}")
77
+ typer.echo(f"{symbol('error')} Cannot remove event: {e}")
75
78
  raise typer.Exit(1)
76
79
 
77
80
 
@@ -7,6 +7,7 @@ import typer
7
7
  from rich.console import Console
8
8
  from rich.panel import Panel
9
9
 
10
+ from microlens_submit.text_symbols import symbol
10
11
  from microlens_submit.utils import load
11
12
 
12
13
  console = Console()
@@ -101,19 +102,21 @@ def init(
101
102
  # Run warnings-only validation
102
103
  warnings = sub.run_validation_warnings()
103
104
  if warnings:
104
- console.print("[yellow]⚠️ Project initialized with warnings:[/yellow]")
105
+ console.print(f"[yellow]{symbol('warning')} Project initialized with warnings:[/yellow]")
105
106
  for warning in warnings:
106
107
  console.print(f" [yellow]• {warning}[/yellow]")
107
- console.print("[yellow]💡 These warnings will become errors when saving or exporting.[/yellow]")
108
+ console.print(f"[yellow]{symbol('hint')} These warnings will become errors when saving or exporting.[/yellow]")
108
109
 
109
110
  # Try to save, but don't fail if there are validation errors
110
111
  try:
111
112
  sub.save()
112
113
  console.print(Panel(f"Initialized project at {project_path}", style="bold green"))
113
114
  except ValueError as e:
114
- console.print("[yellow]⚠️ Project initialized but could not save due to validation errors:[/yellow]")
115
+ console.print(
116
+ f"[yellow]{symbol('warning')} Project initialized but could not save due to validation errors:[/yellow]"
117
+ )
115
118
  console.print(f"[yellow]{str(e)}[/yellow]")
116
- console.print("[yellow]💡 Fix validation errors before saving or exporting.[/yellow]")
119
+ console.print(f"[yellow]{symbol('hint')} Fix validation errors before saving or exporting.[/yellow]")
117
120
  console.print(Panel(f"Initialized project at {project_path} (unsaved)", style="bold yellow"))
118
121
 
119
122
 
@@ -156,17 +159,19 @@ def nexus_init(
156
159
  # Run warnings-only validation after adding hardware info
157
160
  warnings = sub.run_validation_warnings()
158
161
  if warnings:
159
- console.print("[yellow]⚠️ Project updated with warnings:[/yellow]")
162
+ console.print(f"[yellow]{symbol('warning')} Project updated with warnings:[/yellow]")
160
163
  for warning in warnings:
161
164
  console.print(f" [yellow]• {warning}[/yellow]")
162
- console.print("[yellow]💡 These warnings will become errors when saving or exporting.[/yellow]")
165
+ console.print(f"[yellow]{symbol('hint')} These warnings will become errors when saving or exporting.[/yellow]")
163
166
 
164
167
  # Try to save, but don't fail if there are validation errors
165
168
  try:
166
169
  sub.save()
167
170
  console.print("Nexus platform info captured.", style="bold green")
168
171
  except ValueError as e:
169
- console.print("[yellow]⚠️ Project updated but could not save due to validation errors:[/yellow]")
172
+ console.print(
173
+ f"[yellow]{symbol('warning')} Project updated but could not save due to validation errors:[/yellow]"
174
+ )
170
175
  console.print(f"[yellow]{str(e)}[/yellow]")
171
- console.print("[yellow]💡 Fix validation errors before saving or exporting.[/yellow]")
176
+ console.print(f"[yellow]{symbol('hint')} Fix validation errors before saving or exporting.[/yellow]")
172
177
  console.print("Nexus platform info captured (unsaved).", style="bold yellow")
@@ -10,6 +10,7 @@ from rich.console import Console
10
10
  from rich.panel import Panel
11
11
 
12
12
  from microlens_submit.error_messages import enhance_validation_messages, format_cli_error_with_suggestions
13
+ from microlens_submit.text_symbols import symbol
13
14
  from microlens_submit.utils import import_solutions_from_csv, load
14
15
 
15
16
  console = Console()
@@ -319,7 +320,7 @@ def add_solution(
319
320
  console.print(f" • {msg}")
320
321
  else:
321
322
  console.print(
322
- f" Solution {sol.solution_id} created successfully!",
323
+ f"{symbol('check')} Solution {sol.solution_id} created successfully!",
323
324
  style="green",
324
325
  )
325
326
 
@@ -362,7 +363,7 @@ def activate(
362
363
 
363
364
  solution.activate()
364
365
  submission.save()
365
- console.print(f"[green] Activated solution {solution_id[:8]}... in event {event_id}[/green]")
366
+ console.print(f"[green]{symbol('check')} Activated solution {solution_id[:8]}... in event {event_id}[/green]")
366
367
 
367
368
 
368
369
  def remove_solution(
@@ -390,14 +391,17 @@ def remove_solution(
390
391
  removed = submission.events[event_id].remove_solution(solution_id, force=force)
391
392
  if removed:
392
393
  submission.save()
393
- console.print(f"[green]✅ Solution {solution_id[:8]}... removed from event " f"{event_id}[/green]")
394
+ console.print(
395
+ f"[green]{symbol('check')} Solution {solution_id[:8]}... removed from event {event_id}[/green]"
396
+ )
394
397
  else:
395
398
  console.print(f"[red]Error: Failed to remove solution {solution_id}[/red]")
396
399
  raise typer.Exit(1)
397
400
  except ValueError as e:
398
401
  console.print(f"[red]Error: {e}[/red]")
399
402
  console.print(
400
- "[yellow]💡 Use --force to override safety checks, or use deactivate to " "keep the solution[/yellow]"
403
+ f"[yellow]{symbol('hint')} Use --force to override safety checks, "
404
+ "or use deactivate to keep the solution[/yellow]"
401
405
  )
402
406
  raise typer.Exit(1)
403
407
 
@@ -482,10 +486,11 @@ def edit_solution(
482
486
  if target_solution is None:
483
487
  console.print(f"Solution {solution_id} not found", style="bold red")
484
488
  raise typer.Exit(code=1)
489
+ arrow = symbol("arrow")
485
490
  changes = []
486
491
  if alias is not None:
487
492
  if target_solution.alias != alias:
488
- changes.append(f"Update alias: {target_solution.alias} {alias}")
493
+ changes.append(f"Update alias: {target_solution.alias} {arrow} {alias}")
489
494
  target_solution.alias = alias
490
495
  if clear_relative_probability:
491
496
  if target_solution.relative_probability is not None:
@@ -494,7 +499,7 @@ def edit_solution(
494
499
  elif relative_probability is not None:
495
500
  if target_solution.relative_probability != relative_probability:
496
501
  changes.append(
497
- f"Update relative_probability: " f"{target_solution.relative_probability} " f"→ {relative_probability}"
502
+ f"Update relative_probability: {target_solution.relative_probability} {arrow} {relative_probability}"
498
503
  )
499
504
  target_solution.relative_probability = relative_probability
500
505
  if clear_log_likelihood:
@@ -503,7 +508,7 @@ def edit_solution(
503
508
  target_solution.log_likelihood = None
504
509
  elif log_likelihood is not None:
505
510
  if target_solution.log_likelihood != log_likelihood:
506
- changes.append(f"Update log_likelihood: {target_solution.log_likelihood} " f"→ {log_likelihood}")
511
+ changes.append(f"Update log_likelihood: {target_solution.log_likelihood} {arrow} {log_likelihood}")
507
512
  target_solution.log_likelihood = log_likelihood
508
513
  if clear_n_data_points:
509
514
  if target_solution.n_data_points is not None:
@@ -511,7 +516,7 @@ def edit_solution(
511
516
  target_solution.n_data_points = None
512
517
  elif n_data_points is not None:
513
518
  if target_solution.n_data_points != n_data_points:
514
- changes.append(f"Update n_data_points: {target_solution.n_data_points} " f"→ {n_data_points}")
519
+ changes.append(f"Update n_data_points: {target_solution.n_data_points} {arrow} {n_data_points}")
515
520
  target_solution.n_data_points = n_data_points
516
521
  # Notes file logic
517
522
  canonical_notes_path = (
@@ -550,9 +555,9 @@ def edit_solution(
550
555
  old_cpu = target_solution.compute_info.get("cpu_hours")
551
556
  old_wall = target_solution.compute_info.get("wall_time_hours")
552
557
  if cpu_hours is not None and old_cpu != cpu_hours:
553
- changes.append(f"Update cpu_hours: {old_cpu} {cpu_hours}")
558
+ changes.append(f"Update cpu_hours: {old_cpu} {arrow} {cpu_hours}")
554
559
  if wall_time_hours is not None and old_wall != wall_time_hours:
555
- changes.append(f"Update wall_time_hours: {old_wall} {wall_time_hours}")
560
+ changes.append(f"Update wall_time_hours: {old_wall} {arrow} {wall_time_hours}")
556
561
  target_solution.set_compute_info(
557
562
  cpu_hours=cpu_hours if cpu_hours is not None else old_cpu,
558
563
  wall_time_hours=(wall_time_hours if wall_time_hours is not None else old_wall),
@@ -568,7 +573,7 @@ def edit_solution(
568
573
  new_value = value
569
574
  old_value = target_solution.parameters.get(key)
570
575
  if old_value != new_value:
571
- changes.append(f"Update parameter {key}: {old_value} {new_value}")
576
+ changes.append(f"Update parameter {key}: {old_value} {arrow} {new_value}")
572
577
  target_solution.parameters[key] = new_value
573
578
  if param_uncertainty:
574
579
  if target_solution.parameter_uncertainties is None:
@@ -583,7 +588,7 @@ def edit_solution(
583
588
  new_value = value
584
589
  old_value = target_solution.parameter_uncertainties.get(key)
585
590
  if old_value != new_value:
586
- changes.append(f"Update uncertainty {key}: {old_value} {new_value}")
591
+ changes.append(f"Update uncertainty {key}: {old_value} {arrow} {new_value}")
587
592
  target_solution.parameter_uncertainties[key] = new_value
588
593
  if clear_higher_order_effects:
589
594
  if target_solution.higher_order_effects:
@@ -592,7 +597,7 @@ def edit_solution(
592
597
  elif higher_order_effect:
593
598
  if target_solution.higher_order_effects != higher_order_effect:
594
599
  changes.append(
595
- f"Update higher_order_effects: " f"{target_solution.higher_order_effects} " f"→ {higher_order_effect}"
600
+ f"Update higher_order_effects: {target_solution.higher_order_effects} {arrow} {higher_order_effect}"
596
601
  )
597
602
  target_solution.higher_order_effects = higher_order_effect
598
603
  if dry_run:
@@ -670,14 +675,14 @@ def import_solutions(
670
675
  ) -> None:
671
676
  """Import solutions from a CSV file into the current project."""
672
677
  if on_duplicate not in ["error", "override", "ignore"]:
673
- typer.echo(f" Invalid --on-duplicate option: {on_duplicate}")
678
+ typer.echo(f"{symbol('error')} Invalid --on-duplicate option: {on_duplicate}")
674
679
  typer.echo(" Valid options: error, override, ignore")
675
680
  raise typer.Exit(1)
676
681
 
677
682
  try:
678
683
  submission = load(str(project_path))
679
684
  except Exception as e: # pragma: no cover - unexpected I/O errors
680
- typer.echo(f" Failed to load submission: {e}")
685
+ typer.echo(f"{symbol('error')} Failed to load submission: {e}")
681
686
  raise typer.Exit(1)
682
687
 
683
688
  try:
@@ -692,17 +697,17 @@ def import_solutions(
692
697
  project_path=project_path,
693
698
  )
694
699
  except Exception as e: # pragma: no cover - unexpected parse errors
695
- typer.echo(f" Failed to import solutions: {e}")
700
+ typer.echo(f"{symbol('error')} Failed to import solutions: {e}")
696
701
  raise typer.Exit(1)
697
702
 
698
703
  if not dry_run and stats["successful_imports"] > 0:
699
704
  try:
700
705
  submission.save()
701
706
  except Exception as e: # pragma: no cover - disk failures
702
- typer.echo(f" Failed to save submission: {e}")
707
+ typer.echo(f"{symbol('error')} Failed to save submission: {e}")
703
708
  raise typer.Exit(1)
704
709
 
705
- typer.echo("\n📊 Import Summary:")
710
+ typer.echo(f"\n{symbol('progress')} Import Summary:")
706
711
  typer.echo(f" Total rows processed: {stats['total_rows']}")
707
712
  typer.echo(f" Successful imports: {stats['successful_imports']}")
708
713
  typer.echo(f" Skipped rows: {stats['skipped_rows']}")
@@ -710,13 +715,13 @@ def import_solutions(
710
715
  typer.echo(f" Duplicates handled: {stats['duplicate_handled']}")
711
716
 
712
717
  if stats["errors"]:
713
- typer.echo("\n⚠️ Errors encountered:")
718
+ typer.echo(f"\n{symbol('warning')} Errors encountered:")
714
719
  for error in stats["errors"][:10]:
715
720
  typer.echo(f" {error}")
716
721
  if len(stats["errors"]) > 10:
717
722
  typer.echo(f" ... and {len(stats['errors']) - 10} more errors")
718
723
 
719
724
  if dry_run:
720
- typer.echo("\n🔍 Dry run completed - no changes made")
725
+ typer.echo(f"\n{symbol('search')} Dry run completed - no changes made")
721
726
  else:
722
- typer.echo("\n Import completed successfully")
727
+ typer.echo(f"\n{symbol('check')} Import completed successfully")
@@ -10,6 +10,7 @@ from rich.panel import Panel
10
10
  from rich.table import Table
11
11
 
12
12
  from microlens_submit.error_messages import enhance_validation_messages
13
+ from microlens_submit.text_symbols import symbol
13
14
  from microlens_submit.utils import load
14
15
 
15
16
  console = Console()
@@ -42,7 +43,7 @@ def validate_solution(
42
43
  if not enhanced_messages:
43
44
  console.print(
44
45
  Panel(
45
- f" All validations passed for {solution_id} (event {target_event_id})",
46
+ f"{symbol('check')} All validations passed for {solution_id} (event {target_event_id})",
46
47
  style="bold green",
47
48
  )
48
49
  )
@@ -65,7 +66,7 @@ def validate_submission(
65
66
  warnings = sub.run_validation_warnings()
66
67
 
67
68
  if not warnings:
68
- console.print(Panel("\u2705 All validations passed!", style="bold green"))
69
+ console.print(Panel(f"{symbol('check')} All validations passed!", style="bold green"))
69
70
  else:
70
71
  console.print(Panel("Validation Warnings", style="yellow"))
71
72
  for warning in warnings:
@@ -78,21 +79,24 @@ def validate_submission(
78
79
 
79
80
  if has_repo_issue:
80
81
  console.print(
81
- "\n[blue]💡 To fix repository URL issues:[/blue]\n"
82
+ f"\n[blue]{symbol('hint')} To fix repository URL issues:[/blue]\n"
82
83
  " microlens-submit set-repo-url <url> <project_dir>",
83
84
  style="blue",
84
85
  )
85
86
 
86
87
  if has_hardware_issue:
87
88
  console.print(
88
- "\n[blue]💡 To fix hardware info issues:[/blue]\n"
89
+ f"\n[blue]{symbol('hint')} To fix hardware info issues:[/blue]\n"
89
90
  " microlens-submit nexus-init --team-name <name> --tier <tier> <project_dir>\n"
90
91
  " (or manually set hardware_info in submission.json)",
91
92
  style="blue",
92
93
  )
93
94
 
94
95
  console.print(
95
- "\n[yellow]⚠️ Note: These warnings will become errors when saving or exporting.[/yellow]", style="yellow"
96
+ "\n[yellow]"
97
+ f"{symbol('warning')} Note: These warnings will become errors when saving or exporting."
98
+ "[/yellow]",
99
+ style="yellow",
96
100
  )
97
101
 
98
102
 
@@ -121,10 +125,10 @@ def validate_event(
121
125
  console.print(f" • {msg}")
122
126
  all_messages.append(f"{solution.solution_id}: {msg}")
123
127
  else:
124
- console.print(f" Solution {solution.solution_id}: All validations passed")
128
+ console.print(f"{symbol('check')} Solution {solution.solution_id}: All validations passed")
125
129
 
126
130
  if not all_messages:
127
- console.print(Panel(" All solutions passed validation!", style="bold green"))
131
+ console.print(Panel(f"{symbol('check')} All solutions passed validation!", style="bold green"))
128
132
  else:
129
133
  console.print(
130
134
  f"\nFound {len(all_messages)} validation issue(s) across all solutions",
@@ -11,8 +11,12 @@ import webbrowser
11
11
  from datetime import datetime
12
12
  from pathlib import Path
13
13
 
14
- import importlib_resources
14
+ try: # Prefer stdlib importlib.resources when available (Python >= 3.9)
15
+ import importlib.resources as importlib_resources
16
+ except ImportError: # pragma: no cover - fallback for Python < 3.9
17
+ import importlib_resources
15
18
 
19
+ from .. import __version__
16
20
  from ..models.submission import Submission
17
21
  from .utils import extract_github_repo_name, format_hardware_info
18
22
 
@@ -485,7 +489,7 @@ style="width: {progress_percentage}%"></div>
485
489
 
486
490
  <!-- Footer -->
487
491
  <div class="text-sm text-gray-500 text-center pt-8 pb-6">
488
- Generated by microlens-submit v0.16.0 on
492
+ Generated by microlens-submit v{__version__} on
489
493
  {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
490
494
  </div>
491
495
 
@@ -9,6 +9,7 @@ evaluator-only visualizations.
9
9
  from datetime import datetime
10
10
  from pathlib import Path
11
11
 
12
+ from .. import __version__
12
13
  from ..models import Event, Submission
13
14
  from .solution_page import generate_solution_page
14
15
 
@@ -357,7 +358,7 @@ Event {event.event_id}. Points colored by Relative Probability (Evaluator View).
357
358
 
358
359
  <!-- Footer -->
359
360
  <div class='text-sm text-gray-500 text-center pt-8 border-t border-gray-200 mt-10'>
360
- Generated by microlens-submit v0.13.0 on {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
361
+ Generated by microlens-submit v{__version__} on {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
361
362
  </div>
362
363
 
363
364
  <!-- Regex Finish -->
@@ -11,6 +11,7 @@ from pathlib import Path
11
11
 
12
12
  import markdown
13
13
 
14
+ from .. import __version__
14
15
  from ..models.event import Event
15
16
  from ..models.solution import Solution
16
17
  from ..models.submission import Submission
@@ -521,7 +522,7 @@ def _generate_solution_page_content(solution: Solution, event: Event, submission
521
522
 
522
523
  <!-- Footer -->
523
524
  <div class='text-sm text-gray-500 text-center pt-8 border-t border-gray-200 mt-10'>
524
- Generated by microlens-submit v0.16.0 on {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
525
+ Generated by microlens-submit v{__version__} on {datetime.now().strftime('%Y-%m-%d %H:%M:%S UTC')}
525
526
  </div>
526
527
 
527
528
  <!-- Regex Finish -->
@@ -8,6 +8,8 @@ to help users understand and fix issues more easily.
8
8
  import re
9
9
  from typing import Dict, List, Optional
10
10
 
11
+ from microlens_submit.text_symbols import symbol
12
+
11
13
 
12
14
  def get_model_type_suggestions(invalid_type: str) -> List[str]:
13
15
  """Get suggestions for model type corrections."""
@@ -156,9 +158,9 @@ def format_validation_message(message: str, suggestions: Optional[List[str]] = N
156
158
 
157
159
  formatted = message
158
160
  if len(suggestions) == 1:
159
- formatted += f"\n💡 Suggestion: {suggestions[0]}"
161
+ formatted += f"\n{symbol('hint')} Suggestion: {suggestions[0]}"
160
162
  else:
161
- formatted += "\n💡 Suggestions:"
163
+ formatted += f"\n{symbol('hint')} Suggestions:"
162
164
  for suggestion in suggestions:
163
165
  formatted += f"\n • {suggestion}"
164
166
 
@@ -259,7 +261,7 @@ def format_cli_error_with_suggestions(error_message: str, context: Dict) -> str:
259
261
 
260
262
  # Add context-specific suggestions
261
263
  if "model_type must be one of" in error_message:
262
- enhanced += "\n\n💡 Quick Start Examples:"
264
+ enhanced += f"\n\n{symbol('hint')} Quick Start Examples:"
263
265
  enhanced += (
264
266
  "\n microlens-submit add-solution EVENT123 1S1L " "--param t0=2459123.5 --param u0=0.1 --param tE=20.0"
265
267
  )
@@ -275,7 +277,7 @@ def format_cli_error_with_suggestions(error_message: str, context: Dict) -> str:
275
277
  enhanced += "\n Multiple parameters: --param t0=2459123.5 " "--param u0=0.1 --param tE=20.0"
276
278
 
277
279
  elif "Cannot use --param with --params-file" in error_message:
278
- enhanced += "\n\n💡 Parameter Options:"
280
+ enhanced += f"\n\n{symbol('hint')} Parameter Options:"
279
281
  enhanced += "\n Use --param for individual parameters on command line"
280
282
  enhanced += "\n Use --params-file for parameters from a JSON/YAML file"
281
283
  enhanced += "\n Choose one method, not both"
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional
11
11
 
12
12
  from pydantic import BaseModel, Field
13
13
 
14
+ from ..text_symbols import symbol
14
15
  from .solution import Solution
15
16
 
16
17
  if TYPE_CHECKING:
@@ -122,11 +123,11 @@ class Event(BaseModel):
122
123
 
123
124
  # Provide feedback about the created solution
124
125
  alias_info = f" with alias '{alias}'" if alias else ""
125
- print(f" Created solution {solution_id}{alias_info}")
126
+ print(f"{symbol('check')} Created solution {solution_id}{alias_info}")
126
127
  print(f" Model: {model_type}, Parameters: {len(parameters)}")
127
128
  if alias:
128
- print(f" ⚠️ Note: Alias '{alias}' will be validated for uniqueness when saved")
129
- print(" 💾 Remember to call submission.save() to persist to disk")
129
+ print(f" {symbol('warning')} Note: Alias '{alias}' will be validated for uniqueness when saved")
130
+ print(f" {symbol('save')} Remember to call submission.save() to persist to disk")
130
131
 
131
132
  return sol
132
133
 
@@ -322,14 +323,14 @@ class Event(BaseModel):
322
323
  try:
323
324
  if full_path.exists():
324
325
  full_path.unlink()
325
- print(f"🗑️ Removed temporary notes file: {notes_path}")
326
+ print(f"{symbol('trash')} Removed temporary notes file: {notes_path}")
326
327
  except OSError:
327
- print(f"⚠️ Warning: Could not remove temporary file {notes_path}")
328
+ print(f"{symbol('warning')} Warning: Could not remove temporary file {notes_path}")
328
329
 
329
330
  # Remove from solutions dict
330
331
  del self.solutions[solution_id]
331
332
 
332
- print(f"🗑️ Removed solution {solution_id[:8]}... from event {self.event_id}")
333
+ print(f"{symbol('trash')} Removed solution {solution_id[:8]}... from event {self.event_id}")
333
334
  return True
334
335
 
335
336
  def remove_all_solutions(self, force: bool = False) -> int:
@@ -366,7 +367,9 @@ class Event(BaseModel):
366
367
  removed_count += 1
367
368
  except ValueError:
368
369
  if not force:
369
- print(f"⚠️ Skipped saved solution {solution_id[:8]}... (use force=True to remove)")
370
+ print(
371
+ f"{symbol('warning')} Skipped saved solution {solution_id[:8]}... (use force=True to remove)"
372
+ )
370
373
  else:
371
374
  # Force=True should override the saved check
372
375
  if self.remove_solution(solution_id, force=True):
@@ -15,6 +15,7 @@ from typing import Dict, List, Optional
15
15
 
16
16
  from pydantic import BaseModel, Field
17
17
 
18
+ from ..text_symbols import symbol
18
19
  from .event import Event
19
20
  from .solution import Solution
20
21
 
@@ -348,22 +349,22 @@ class Submission(BaseModel):
348
349
 
349
350
  def print_solution_status(self) -> None:
350
351
  status = self.get_solution_status()
351
- print("📊 Solution Status Summary:")
352
+ print(f"{symbol('progress')} Solution Status Summary:")
352
353
  print(f" Total solutions: {status['total']}")
353
354
  print(f" Saved to disk: {status['saved']}")
354
355
  print(f" Unsaved (in memory): {status['unsaved']}")
355
356
  if status["unsaved"] > 0:
356
- print(" 💾 Call submission.save() to persist unsaved solutions")
357
+ print(f" {symbol('save')} Call submission.save() to persist unsaved solutions")
357
358
  if status["duplicate_aliases"]:
358
- print(" Alias conflicts found:")
359
+ print(f" {symbol('error')} Alias conflicts found:")
359
360
  for error in status["duplicate_aliases"]:
360
361
  print(f" {error}")
361
- print(" 💡 Resolve conflicts before saving")
362
+ print(f" {symbol('hint')} Resolve conflicts before saving")
362
363
  for event_id, event_status in status["events"].items():
363
- print(f"\n📁 Event {event_id}:")
364
+ print(f"\n{symbol('folder')} Event {event_id}:")
364
365
  print(f" Solutions: {event_status['saved']} saved, {event_status['unsaved']} unsaved")
365
366
  for sol_id, sol_status in event_status["solutions"].items():
366
- status_icon = "" if sol_status["saved"] else ""
367
+ status_icon = symbol("check") if sol_status["saved"] else symbol("pending")
367
368
  alias_info = f" (alias: {sol_status['alias']})" if sol_status["alias"] else ""
368
369
  active_info = "" if sol_status["is_active"] else " [inactive]"
369
370
  print(f" {status_icon} {sol_id} - {sol_status['model_type']}{alias_info}{active_info}")
@@ -372,25 +373,25 @@ class Submission(BaseModel):
372
373
  # Run comprehensive validation first
373
374
  validation_errors = self.run_validation()
374
375
  if validation_errors:
375
- print("⚠️ Save completed with validation warnings:")
376
+ print(f"{symbol('warning')} Save completed with validation warnings:")
376
377
  for error in validation_errors:
377
378
  print(f" {error}")
378
- print("💡 Fix validation errors before exporting for submission")
379
+ print(f"{symbol('hint')} Fix validation errors before exporting for submission")
379
380
 
380
381
  if not force:
381
- print("💾 Submission saved locally (incomplete - not ready for submission)")
382
+ print(f"{symbol('save')} Submission saved locally (incomplete - not ready for submission)")
382
383
  else:
383
- print("💾 Submission saved locally (forced save with validation errors)")
384
+ print(f"{symbol('save')} Submission saved locally (forced save with validation errors)")
384
385
  else:
385
- print(" Submission saved successfully (ready for export)")
386
+ print(f"{symbol('check')} Submission saved successfully (ready for export)")
386
387
 
387
388
  # Check for alias conflicts (existing behavior)
388
389
  alias_errors = self._validate_alias_uniqueness()
389
390
  if alias_errors:
390
- print(" Save failed due to alias validation errors:")
391
+ print(f"{symbol('error')} Save failed due to alias validation errors:")
391
392
  for error in alias_errors:
392
393
  print(f" {error}")
393
- print("💡 Solutions with duplicate aliases remain in memory but are not saved")
394
+ print(f"{symbol('hint')} Solutions with duplicate aliases remain in memory but are not saved")
394
395
  print(" Use different aliases or remove aliases to resolve conflicts")
395
396
  raise ValueError("Alias validation failed:\n" + "\n".join(alias_errors))
396
397
 
@@ -420,9 +421,9 @@ class Submission(BaseModel):
420
421
  for sol in event.solutions.values():
421
422
  sol.saved = True
422
423
  if unsaved_count > 0:
423
- print(f" Successfully saved {unsaved_count} new solution(s) to disk")
424
+ print(f"{symbol('check')} Successfully saved {unsaved_count} new solution(s) to disk")
424
425
  else:
425
- print(" Successfully saved submission to disk")
426
+ print(f"{symbol('check')} Successfully saved submission to disk")
426
427
  saved_aliases = [
427
428
  f"{event_id} {sol.alias}"
428
429
  for event_id, event in self.events.items()
@@ -430,17 +431,17 @@ class Submission(BaseModel):
430
431
  if sol.alias and sol.saved
431
432
  ]
432
433
  if saved_aliases:
433
- print(f"📋 Saved aliases: {', '.join(saved_aliases)}")
434
+ print(f"{symbol('clipboard')} Saved aliases: {', '.join(saved_aliases)}")
434
435
 
435
436
  def export(self, output_path: str) -> None:
436
437
  # Run comprehensive validation first - export is strict
437
438
  validation_errors = self.run_validation()
438
439
  if validation_errors:
439
- print(" Export failed due to validation errors:")
440
+ print(f"{symbol('error')} Export failed due to validation errors:")
440
441
  for error in validation_errors:
441
442
  print(f" {error}")
442
- print("💡 Fix validation errors before exporting for submission")
443
- print("💡 Use submission.save() to save incomplete work locally")
443
+ print(f"{symbol('hint')} Fix validation errors before exporting for submission")
444
+ print(f"{symbol('hint')} Use submission.save() to save incomplete work locally")
444
445
  raise ValueError("Validation failed:\n" + "\n".join(validation_errors))
445
446
 
446
447
  project = Path(self.project_path)
@@ -559,11 +560,11 @@ class Submission(BaseModel):
559
560
  try:
560
561
  if full_path.exists():
561
562
  full_path.unlink()
562
- print(f"🗑️ Removed temporary notes file: {notes_path}")
563
+ print(f"{symbol('trash')} Removed temporary notes file: {notes_path}")
563
564
  except OSError as e:
564
- print(f"⚠️ Warning: Could not remove temporary file {notes_path}: {e}")
565
+ print(f"{symbol('warning')} Warning: Could not remove temporary file {notes_path}: {e}")
565
566
  del self.events[event_id]
566
- print(f"🗑️ Removed event '{event_id}' with {len(event.solutions)} solutions")
567
+ print(f"{symbol('trash')} Removed event '{event_id}' with {len(event.solutions)} solutions")
567
568
  return True
568
569
 
569
570
  # ... (all methods from Submission class, unchanged, including docstrings)
@@ -0,0 +1,75 @@
1
+ """Console-friendly symbols with graceful fallbacks.
2
+
3
+ Some Windows terminals still use legacy code pages (e.g., CP1252) that cannot
4
+ encode emoji-style glyphs. Attempting to print those characters raises
5
+ ``UnicodeEncodeError``. This module exposes a :func:`symbol` helper that
6
+ returns the Unicode glyph when the current output stream supports it and
7
+ otherwise falls back to a readable ASCII alternative.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from typing import Dict, Tuple
14
+
15
+ _SYMBOL_MAP: Dict[str, Tuple[str, str]] = {
16
+ "check": ("✅", "[OK]"),
17
+ "warning": ("⚠️", "[WARN]"),
18
+ "error": ("❌", "[ERROR]"),
19
+ "save": ("💾", "[SAVE]"),
20
+ "progress": ("📊", "[STATUS]"),
21
+ "folder": ("📁", "[EVENT]"),
22
+ "pending": ("⏳", "[PENDING]"),
23
+ "clipboard": ("📋", "[ALIASES]"),
24
+ "hint": ("💡", "[HINT]"),
25
+ "note": ("💡", "[NOTE]"),
26
+ "trash": ("🗑️", "[REMOVE]"),
27
+ "search": ("🔍", "[DRY RUN]"),
28
+ "arrow": ("→", "->"),
29
+ }
30
+
31
+
32
+ def _can_encode(text: str) -> bool:
33
+ """Return ``True`` if ``text`` can be encoded using the current stdout/stderr."""
34
+
35
+ def _stream_supports(stream) -> bool | None:
36
+ if stream is None:
37
+ return None
38
+ encoding = getattr(stream, "encoding", None)
39
+ if not encoding:
40
+ return None
41
+ try:
42
+ text.encode(encoding)
43
+ except UnicodeEncodeError:
44
+ return False
45
+ else:
46
+ return True
47
+
48
+ # Prefer the actively used stdout/stderr streams.
49
+ for attr in ("stdout", "stderr"):
50
+ result = _stream_supports(getattr(sys, attr, None))
51
+ if result is not None:
52
+ return result
53
+
54
+ # Fall back to the original streams if the active ones are unavailable.
55
+ for attr in ("__stdout__", "__stderr__"):
56
+ result = _stream_supports(getattr(sys, attr, None))
57
+ if result:
58
+ return True
59
+ return False
60
+
61
+
62
+ def symbol(name: str) -> str:
63
+ """Return a console-friendly symbol for ``name``.
64
+
65
+ Args:
66
+ name: Symbol identifier such as ``"check"`` or ``"warning"``.
67
+
68
+ Returns:
69
+ The preferred Unicode glyph when supported, otherwise a readable ASCII
70
+ fallback.
71
+ """
72
+ unicode_char, fallback = _SYMBOL_MAP.get(name, ("", ""))
73
+ if unicode_char and _can_encode(unicode_char):
74
+ return unicode_char
75
+ return fallback or unicode_char
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: microlens-submit
3
- Version: 0.16.0
3
+ Version: 0.16.2
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
@@ -41,6 +41,11 @@ Requires-Dist: isort; extra == "dev"
41
41
  Requires-Dist: sphinx; extra == "dev"
42
42
  Requires-Dist: sphinx_rtd_theme; extra == "dev"
43
43
  Requires-Dist: importlib_resources; extra == "dev"
44
+ Requires-Dist: python-dotenv; extra == "dev"
45
+ Dynamic: author
46
+ Dynamic: home-page
47
+ Dynamic: license-file
48
+ Dynamic: requires-python
44
49
 
45
50
  <p align="center">
46
51
  <a href="https://github.com/AmberLee2427/microlens-submit">
@@ -52,11 +57,7 @@ Requires-Dist: importlib_resources; extra == "dev"
52
57
 
53
58
  *A stateful submission toolkit for the RGES-PIT Microlensing Data Challenge.*
54
59
 
55
- [![PyPI - v0.16.0](https://img.shields.io/pypi/v/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
56
- [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
57
- [![PyPI - License](https://img.shields.io/pypi/l/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)
58
- [![CI](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml/badge.svg)](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml)
59
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
60
+ [![PyPI - v0.16.0](https://img.shields.io/pypi/v/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)[![Read the Docs](https://readthedocs.org/projects/microlens-submit/badge/?version=latest)](https://microlens-submit.readthedocs.io/en/latest/?badge=latest)[![CI](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml/badge.svg)](https://github.com/AmberLee2427/microlens-submit/actions/workflows/ci.yml)[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/microlens-submit.svg)](https://pypi.org/project/microlens-submit/)[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
60
61
 
61
62
  <br>
62
63
 
@@ -93,8 +94,21 @@ The CLI is the recommended way to interact with your submission project.
93
94
 
94
95
  You can pass ``--no-color`` to any command if your terminal does not support ANSI colors.
95
96
 
96
- 1. Initialize your project: `microlens-submit init --team-name "Planet Pounders" --tier "advanced"`
97
+ 1. Initialize your project:
98
+
99
+ ```bash
100
+ microlens-submit init --team-name "Planet Pounders" --tier "advanced"
101
+ # if a project directory was provided to `init`, you should now `cd` into that project
102
+ ```
103
+
104
+ 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.
105
+
106
+ ```bash
107
+ microlens-submit set-repo-url <url> ./
108
+ microlens-submit set-hardware-info --cpu-details "intel i7 xxx" --ram-gb 32 ./
109
+ ```
97
110
  2. Add a new solution to an event:
111
+
98
112
  ```bash
99
113
  microlens-submit add-solution ogle-2025-blg-0042 1S2L \
100
114
  --param t0=555.5 \
@@ -102,18 +116,28 @@ You can pass ``--no-color`` to any command if your terminal does not support ANS
102
116
  --param tE=25.0 \
103
117
  --notes "This is a great fit!"
104
118
  ```
119
+
105
120
  Model types must be one of `1S1L`, `1S2L`, `2S1L`, `2S2L`, `1S3L`, `2S3L`, or `other`.
106
121
  This will create a new solution and print its unique `solution_id`.
122
+
107
123
  You can run the same command with `--dry-run` first to verify the
108
124
  parsed input without saving anything.
125
+
109
126
  3. **Bulk import multiple solutions from a CSV file:**
127
+
110
128
  ```bash
111
129
  microlens-submit import-solutions tests/data/test_import.csv --dry-run
112
130
  ```
113
- See the file `tests/data/test_import.csv` for a comprehensive example covering all features and edge cases. You can use this file as a template for your own imports.
131
+
132
+ See the file `tests/data/test_import.csv` for a comprehensive example covering all features and edge cases.
133
+ You can use this file as a template for your own imports.
134
+
114
135
  4. Deactivate a solution that didn't work out: `microlens-submit deactivate <solution_id>`
136
+
115
137
  5. List all solutions for an event: `microlens-submit list-solutions ogle-2025-blg-0042`
138
+
116
139
  6. Validate solutions and check for issues: `microlens-submit validate-solution <solution_id>`
140
+
117
141
  7. Export your final submission: `microlens-submit export final_submission.zip`
118
142
 
119
143
  **Note:** When you add a solution, it's automatically validated and any warnings are displayed. Use `--dry-run` to check validation without saving.
@@ -174,3 +198,23 @@ A comprehensive test CSV file is provided at `tests/data/test_import.csv`. This
174
198
  If you use **microlens-submit** in your research, please cite the project using
175
199
  the metadata provided in the `CITATION.cff` file. Most reference managers can
176
200
  import this file directly.
201
+
202
+ Bibtex:
203
+ ```
204
+ @software{malpas_2025_17460500,
205
+ author = {Malpas, Amber},
206
+ title = {microlens-submit},
207
+ month = oct,
208
+ year = 2025,
209
+ publisher = {Zenodo},
210
+ version = {v0.16.1},
211
+ doi = {10.5281/zenodo.17460500},
212
+ url = {https://doi.org/10.5281/zenodo.17460500},
213
+ }
214
+ ```
215
+
216
+ Cite without version:
217
+ Malpas, A. (2025). microlens-submit. Zenodo. https://doi.org/10.5281/zenodo.17459752
218
+
219
+ Cite current version:
220
+ Malpas, A. (2025). microlens-submit (v0.16.1). Zenodo. https://doi.org/10.5281/zenodo.17459753
@@ -5,6 +5,7 @@ pyproject.toml
5
5
  setup.py
6
6
  microlens_submit/__init__.py
7
7
  microlens_submit/error_messages.py
8
+ microlens_submit/text_symbols.py
8
9
  microlens_submit/tier_validation.py
9
10
  microlens_submit/utils.py
10
11
  microlens_submit/validate_parameters.py
@@ -18,3 +18,4 @@ isort
18
18
  sphinx
19
19
  sphinx_rtd_theme
20
20
  importlib_resources
21
+ python-dotenv
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "microlens-submit"
9
- version = "0.16.0"
9
+ version = "0.16.2"
10
10
  authors = [
11
11
  { name="Amber Malpas", email="malpas.1@osu.edu" },
12
12
  {name = "Roman Science Platform Team", email = "roman-science-platform@stsci.edu"}
@@ -36,7 +36,7 @@ dependencies = [
36
36
  "rich>=13.0.0",
37
37
  "pyyaml>=6.0",
38
38
  "markdown>=3.4.0",
39
- "importlib_resources>=1.0.0; python_version<\"3.9\"",
39
+ 'importlib_resources>=1.0.0; python_version < "3.9"',
40
40
  ]
41
41
 
42
42
  [project.urls]
@@ -60,12 +60,13 @@ dev = [
60
60
  "sphinx",
61
61
  "sphinx_rtd_theme",
62
62
  "importlib_resources",
63
+ "python-dotenv",
63
64
  ]
64
65
 
65
66
  # Setuptools package configuration
66
67
  [tool.setuptools.packages.find]
67
68
  where = ["."]
68
- include = ["microlens_submit*"]
69
+ include = ["microlens_submit", "microlens_submit.*"]
69
70
  exclude = ["tests*", "docs*"]
70
71
 
71
72
  [tool.setuptools.package-data]
@@ -8,8 +8,8 @@ from setuptools import find_packages, setup
8
8
  if __name__ == "__main__":
9
9
  setup(
10
10
  name="microlens-submit",
11
- version="0.16.0",
12
- packages=find_packages(),
11
+ version="0.16.2",
12
+ packages=find_packages(include=("microlens_submit", "microlens_submit.*")),
13
13
  include_package_data=True,
14
14
  package_data={
15
15
  "microlens_submit": ["assets/*"],
@@ -61,20 +61,22 @@ Note:
61
61
  The test suite ensures CLI functionality matches API behavior.
62
62
  """
63
63
 
64
- # Install package in editable mode to ensure assets are available
64
+ # Install package in editable mode to ensure assets are available when running tests
65
+ import os
65
66
  import subprocess
66
67
  import sys
67
68
 
68
- try:
69
- subprocess.run(
70
- [sys.executable, "-m", "pip", "install", "-e", "."],
71
- capture_output=True,
72
- check=True,
73
- )
74
- except subprocess.CalledProcessError:
75
- # If we're not in the right directory or package isn't set up, continue anyway
76
- # The asset check fixture will catch missing assets
77
- pass
69
+ if os.environ.get("MICROLENS_SKIP_EDITABLE_INSTALL") != "1":
70
+ try:
71
+ subprocess.run(
72
+ [sys.executable, "-m", "pip", "install", "-e", "."],
73
+ capture_output=True,
74
+ check=True,
75
+ )
76
+ except subprocess.CalledProcessError:
77
+ # If we're not in the right directory or package isn't set up, continue anyway
78
+ # The asset check fixture will catch missing assets
79
+ pass
78
80
 
79
81
  import json
80
82
  import zipfile
@@ -388,10 +390,13 @@ def test_cli_compare_solutions_skips_zero_data_points():
388
390
 
389
391
  result = runner.invoke(app, ["compare-solutions", "evt"])
390
392
  assert result.exit_code == 0
391
- # Should only show the valid solution in the table
392
- # The output includes "Relative probabilities calculated using BIC" in footer
393
- # so we count the exact table header cell for the 'Relative' column
394
- assert result.stdout.count("┃ Relative ┃") == 1
393
+ # Should only show the valid solution in the table. Depending on console support
394
+ # Rich may render Unicode box characters or ASCII fallbacks. Count the header row
395
+ # that contains the "Relative" column (ignoring the footer line that describes BIC).
396
+ header_lines = [
397
+ line for line in result.stdout.splitlines() if "Relative" in line and "Relative probabilities" not in line
398
+ ]
399
+ assert len(header_lines) == 1
395
400
 
396
401
 
397
402
  def test_params_file_option_and_bands():