starbash 0.3.2__tar.gz → 0.3.4__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 (138) hide show
  1. {starbash-0.3.2 → starbash-0.3.4}/PKG-INFO +34 -13
  2. {starbash-0.3.2 → starbash-0.3.4}/README.md +23 -2
  3. {starbash-0.3.2 → starbash-0.3.4}/pyproject.toml +41 -28
  4. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/analytics.py +31 -0
  5. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/app.py +129 -44
  6. starbash-0.3.4/src/starbash/assets/check.png +0 -0
  7. starbash-0.3.4/src/starbash/assets/icon.png +0 -0
  8. starbash-0.3.4/src/starbash/assets/icon.svg +49 -0
  9. starbash-0.3.4/src/starbash/assets/starbash.desktop.in +14 -0
  10. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/check_version.py +1 -1
  11. starbash-0.3.4/src/starbash/commands/gui.py +24 -0
  12. starbash-0.3.4/src/starbash/commands/process.py +834 -0
  13. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/commands/publish.py +38 -109
  14. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/commands/repo.py +14 -3
  15. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/commands/user.py +11 -10
  16. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/database.py +7 -2
  17. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/doit.py +118 -37
  18. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/doit_types.py +1 -3
  19. starbash-0.3.4/src/starbash/events.py +244 -0
  20. starbash-0.3.4/src/starbash/interaction.py +121 -0
  21. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/main.py +43 -0
  22. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/paths.py +20 -1
  23. starbash-0.3.4/src/starbash/preferences.py +26 -0
  24. starbash-0.3.4/src/starbash/processed_target.py +1372 -0
  25. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/processing.py +416 -65
  26. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/processing_like.py +2 -4
  27. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/publish/credentials.py +17 -2
  28. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/publish/github.py +157 -17
  29. starbash-0.3.4/src/starbash/publish/github_publish.py +299 -0
  30. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/publish/github_service.py +46 -29
  31. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/recipes/crop.py +1 -3
  32. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/report.py +7 -5
  33. starbash-0.3.4/src/starbash/rich.py +290 -0
  34. starbash-0.3.4/src/starbash/run_state.py +582 -0
  35. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/score.py +88 -13
  36. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/selection.py +56 -0
  37. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/sim_siril/connection.py +7 -2
  38. starbash-0.3.4/src/starbash/stages.py +481 -0
  39. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/report/target.md.jinja +20 -5
  40. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/userconfig.toml +9 -0
  41. starbash-0.3.4/src/starbash/tool/__init__.py +128 -0
  42. starbash-0.3.4/src/starbash/tool/base.py +876 -0
  43. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/tool/context.py +6 -6
  44. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/tool/graxpert.py +38 -6
  45. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/tool/python.py +52 -39
  46. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/tool/rcastro.py +43 -46
  47. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/tool/siril.py +48 -14
  48. starbash-0.3.4/src/starbash/tool/starnet.py +229 -0
  49. starbash-0.3.4/src/starbash/ui/__init__.py +8 -0
  50. starbash-0.3.4/src/starbash/ui/cli.py +117 -0
  51. starbash-0.3.4/src/starbash/ui/cli_events.py +356 -0
  52. starbash-0.3.4/src/starbash/ui/qt/__init__.py +90 -0
  53. starbash-0.3.4/src/starbash/ui/qt/app.py +77 -0
  54. starbash-0.3.4/src/starbash/ui/qt/bridge.py +67 -0
  55. starbash-0.3.4/src/starbash/ui/qt/desktop.py +156 -0
  56. starbash-0.3.4/src/starbash/ui/qt/interaction.py +83 -0
  57. starbash-0.3.4/src/starbash/ui/qt/jobs.py +289 -0
  58. starbash-0.3.4/src/starbash/ui/qt/main_window.py +293 -0
  59. starbash-0.3.4/src/starbash/ui/qt/models.py +238 -0
  60. starbash-0.3.4/src/starbash/ui/qt/pages/__init__.py +40 -0
  61. starbash-0.3.4/src/starbash/ui/qt/pages/base.py +140 -0
  62. starbash-0.3.4/src/starbash/ui/qt/pages/dashboard.py +64 -0
  63. starbash-0.3.4/src/starbash/ui/qt/pages/masters.py +62 -0
  64. starbash-0.3.4/src/starbash/ui/qt/pages/processing.py +645 -0
  65. starbash-0.3.4/src/starbash/ui/qt/pages/publish.py +198 -0
  66. starbash-0.3.4/src/starbash/ui/qt/pages/repositories.py +232 -0
  67. starbash-0.3.4/src/starbash/ui/qt/pages/sessions.py +141 -0
  68. starbash-0.3.4/src/starbash/ui/qt/pages/settings.py +91 -0
  69. starbash-0.3.4/src/starbash/ui/qt/pages/targets.py +1074 -0
  70. starbash-0.3.4/src/starbash/ui/qt/pages/wizard.py +1056 -0
  71. starbash-0.3.4/src/starbash/ui/qt/services.py +284 -0
  72. starbash-0.3.4/src/starbash/ui/qt/theme.py +491 -0
  73. starbash-0.3.4/src/starbash/ui/qt/widgets/__init__.py +33 -0
  74. starbash-0.3.4/src/starbash/ui/qt/widgets/busy_indicator.py +322 -0
  75. starbash-0.3.4/src/starbash/ui/qt/widgets/file_links.py +154 -0
  76. starbash-0.3.4/src/starbash/ui/qt/widgets/github_login.py +388 -0
  77. starbash-0.3.4/src/starbash/ui/qt/widgets/hover_preview.py +837 -0
  78. starbash-0.3.4/src/starbash/ui/qt/widgets/image_viewer.py +261 -0
  79. starbash-0.3.4/src/starbash/ui/qt/widgets/log_view.py +54 -0
  80. starbash-0.3.4/src/starbash/ui/qt/widgets/master_picker.py +266 -0
  81. starbash-0.3.4/src/starbash/ui/qt/widgets/selection_panel.py +185 -0
  82. starbash-0.3.4/src/starbash/ui/qt/widgets/stat_card.py +42 -0
  83. starbash-0.3.4/src/starbash/ui/qt/widgets/tool_warning.py +171 -0
  84. starbash-0.3.4/src/starbash/ui/qt/workers.py +210 -0
  85. starbash-0.3.4/src/starbash/url.py +37 -0
  86. starbash-0.3.2/src/starbash/commands/process.py +0 -305
  87. starbash-0.3.2/src/starbash/icon.py +0 -0
  88. starbash-0.3.2/src/starbash/processed_target.py +0 -430
  89. starbash-0.3.2/src/starbash/rich.py +0 -90
  90. starbash-0.3.2/src/starbash/stages.py +0 -265
  91. starbash-0.3.2/src/starbash/tool/__init__.py +0 -56
  92. starbash-0.3.2/src/starbash/tool/base.py +0 -548
  93. starbash-0.3.2/src/starbash/tool/starnet.py +0 -78
  94. starbash-0.3.2/src/starbash/ui/main.py +0 -92
  95. starbash-0.3.2/src/starbash/url.py +0 -17
  96. {starbash-0.3.2 → starbash-0.3.4}/LICENSE +0 -0
  97. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/__init__.py +0 -0
  98. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/aliases.py +0 -0
  99. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/assets/__init__.py +0 -0
  100. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/assets/favicon.ico +0 -0
  101. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/commands/__init__.py +0 -0
  102. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/commands/info.py +0 -0
  103. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/commands/select.py +0 -0
  104. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/defaults/__init__.py +0 -0
  105. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/defaults/starbash.toml +0 -0
  106. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/dwarf3.py +0 -0
  107. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/exception.py +0 -0
  108. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/filtering.py +0 -0
  109. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/fits.py +0 -0
  110. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/linux.py +0 -0
  111. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/os.py +0 -0
  112. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/parameters.py +0 -0
  113. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/publish/__init__.py +0 -0
  114. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/recipes/README.md +0 -0
  115. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/recipes/__init__.py +0 -0
  116. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/recipes/osc.py +0 -0
  117. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/recipes/report_registration.py +0 -0
  118. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/safety.py +0 -0
  119. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/sim_siril/__init__.py +0 -0
  120. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/sim_siril/enums.py +0 -0
  121. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/sim_siril/utility.py +0 -0
  122. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/siril/import_registration.py +0 -0
  123. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/stage_utils.py +0 -0
  124. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/__init__.py +0 -0
  125. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/repo/master.toml +0 -0
  126. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/repo/processed.toml +0 -0
  127. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/report/Gemfile +0 -0
  128. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/report/README.md.jinja +0 -0
  129. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/report/__init__.py +0 -0
  130. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/report/_config.yml +0 -0
  131. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/report/default.html +0 -0
  132. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/report/index.md.jinja +0 -0
  133. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/target/master.toml +0 -0
  134. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/target/processed/about.toml +0 -0
  135. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/target/processed/main.toml +0 -0
  136. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/templates/target/processed/sessions.toml +0 -0
  137. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/toml.py +0 -0
  138. {starbash-0.3.2 → starbash-0.3.4}/src/starbash/windows.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: starbash
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: A tool for automating/standardizing/sharing astrophotography workflows.
5
5
  License-File: LICENSE
6
6
  Author: Kevin Hester
@@ -10,23 +10,22 @@ Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Classifier: Programming Language :: Python :: 3.14
13
- Requires-Dist: astropy (>=7.1.1,<8.0.0)
14
- Requires-Dist: click (>=8.0,<8.5)
15
- Requires-Dist: doit (>=0.36.0,<0.37.0)
13
+ Requires-Dist: astropy (>=7.1.1)
14
+ Requires-Dist: doit (>=0.36.0)
16
15
  Requires-Dist: graxpert[cpuonly] (>=3.2.0a2,<4.0.0)
17
16
  Requires-Dist: jinja2 (>=3.1,<4.0)
18
- Requires-Dist: keyring (>=25.6,<26.0)
17
+ Requires-Dist: keyring (>=25.6)
19
18
  Requires-Dist: numpy (==2.2.6)
20
19
  Requires-Dist: platformdirs (>=4.5.0,<5.0.0)
21
20
  Requires-Dist: pygal (>=3.0,<4.0)
22
21
  Requires-Dist: pyqt6 (>=6.10.1,<7.0.0)
23
- Requires-Dist: restrictedpython (>=8.1,<9.0)
24
- Requires-Dist: rich (>=14.2.0,<15.0.0)
25
- Requires-Dist: sentry-sdk (>=2.42.1,<3.0.0)
26
- Requires-Dist: textual (>=6.8.0,<7.0.0)
27
- Requires-Dist: toml-repo (>=0.1.6,<0.2.0)
28
- Requires-Dist: typer (>=0.20.0,<0.21.0)
29
- Requires-Dist: update-checker (>=0.18.0,<0.19.0)
22
+ Requires-Dist: pyside6 (>=6.11.2,<7.0.0)
23
+ Requires-Dist: restrictedpython (>=8.1)
24
+ Requires-Dist: rich (>=14.2.0)
25
+ Requires-Dist: sentry-sdk (>=2.42.1)
26
+ Requires-Dist: toml-repo (>=0.1.7,<0.2.0)
27
+ Requires-Dist: typer (>=0.24.2)
28
+ Requires-Dist: update-checker (>=1.0.0,<2.0.0)
30
29
  Project-URL: Documentation, https://github.com/geeksville/starbash/blob/main/README.md
31
30
  Project-URL: Homepage, https://github.com/geeksville/starbash
32
31
  Project-URL: Repository, https://github.com/geeksville/starbash
@@ -80,7 +79,12 @@ This project is still very young - but making good progress 😊!
80
79
 
81
80
  If you are interested in alpha-testing we ❤️ you. This README should have enough instructions to get you going, but if you encounter **any** problems please file a github issue and we'll work together to fix them.
82
81
 
83
- ![Sample session movie](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
82
+ (new!) We now have a full GUI (click the video to play)...
83
+
84
+ <video src="https://github.com/user-attachments/assets/0791c47f-f8fa-4279-be06-f869d7212d8c" autoplay controls loop muted playsinline width="100%"></video>
85
+
86
+ We have a CLI interface also...
87
+ ![CLI sample session](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
84
88
 
85
89
  ## Current features
86
90
 
@@ -225,6 +229,22 @@ sb publish github # Publish to github
225
229
  See the [GitHub publishing guide](doc/publish/github.md) for prerequisites,
226
230
  preview instructions, authentication, and troubleshooting.
227
231
 
232
+ ### Desktop GUI
233
+
234
+ Prefer clicking to typing? Starbash ships a desktop app (built on PySide6) that
235
+ covers the same ground as the CLI — browse sessions and preview frames, edit the
236
+ selection, run the pipeline and watch every stage live, tweak per-target stage
237
+ exclusions, manage repositories, and build the report site.
238
+
239
+ ```bash
240
+ sb gui
241
+ ```
242
+
243
+ PySide6 is installed with Starbash, so there is nothing extra to add. The GUI is
244
+ optional in the sense that you can ignore it completely and keep driving Starbash
245
+ from the command line: both front ends share the same selection, config and
246
+ database, so you can mix them freely.
247
+
228
248
  ## Supported commands
229
249
 
230
250
  ### Repository Management
@@ -306,3 +326,4 @@ However, in some of my other open-source projects, I've seen the current hell PR
306
326
 
307
327
  Copyright 2025 Kevin Hester, kevinh@geeksville.com.
308
328
  Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
329
+
@@ -46,7 +46,12 @@ This project is still very young - but making good progress 😊!
46
46
 
47
47
  If you are interested in alpha-testing we ❤️ you. This README should have enough instructions to get you going, but if you encounter **any** problems please file a github issue and we'll work together to fix them.
48
48
 
49
- ![Sample session movie](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
49
+ (new!) We now have a full GUI (click the video to play)...
50
+
51
+ <video src="https://github.com/user-attachments/assets/0791c47f-f8fa-4279-be06-f869d7212d8c" autoplay controls loop muted playsinline width="100%"></video>
52
+
53
+ We have a CLI interface also...
54
+ ![CLI sample session](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/doc/vhs/sample-session.gif)
50
55
 
51
56
  ## Current features
52
57
 
@@ -191,6 +196,22 @@ sb publish github # Publish to github
191
196
  See the [GitHub publishing guide](doc/publish/github.md) for prerequisites,
192
197
  preview instructions, authentication, and troubleshooting.
193
198
 
199
+ ### Desktop GUI
200
+
201
+ Prefer clicking to typing? Starbash ships a desktop app (built on PySide6) that
202
+ covers the same ground as the CLI — browse sessions and preview frames, edit the
203
+ selection, run the pipeline and watch every stage live, tweak per-target stage
204
+ exclusions, manage repositories, and build the report site.
205
+
206
+ ```bash
207
+ sb gui
208
+ ```
209
+
210
+ PySide6 is installed with Starbash, so there is nothing extra to add. The GUI is
211
+ optional in the sense that you can ignore it completely and keep driving Starbash
212
+ from the command line: both front ends share the same selection, config and
213
+ database, so you can mix them freely.
214
+
194
215
  ## Supported commands
195
216
 
196
217
  ### Repository Management
@@ -271,4 +292,4 @@ However, in some of my other open-source projects, I've seen the current hell PR
271
292
  ## License
272
293
 
273
294
  Copyright 2025 Kevin Hester, kevinh@geeksville.com.
274
- Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
295
+ Licensed under the [GPL v3](https://raw.githubusercontent.com/geeksville/starbash/refs/heads/main/LICENSE)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "starbash"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "A tool for automating/standardizing/sharing astrophotography workflows."
5
5
  authors = ["Kevin Hester <kevinh@geeksville.com>"]
6
6
  readme = "README.md"
@@ -16,22 +16,19 @@ python = ">=3.12,<3.15" # RestrictedPython doesn't yet work on 3.15
16
16
 
17
17
  # Note: to use toml-repo from the local submodule rather than pypi use:
18
18
  # toml-repo = {path = "toml-repo", develop = true}
19
- rich = ">=14.2.0,<15.0.0"
20
- restrictedpython = ">=8.1,<9.0"
21
- astropy = ">=7.1.1,<8.0.0"
19
+ rich = ">=14.2.0"
20
+ restrictedpython = ">=8.1"
21
+ astropy = ">=7.1.1"
22
22
  platformdirs = ">=4.5.0,<5.0.0"
23
- typer = "^0.20.0"
24
- # Typer 0.20.1 still imports Click's deprecated stream helpers. Click 8.5
25
- # warns about those imports, so keep the compatible pre-8.5 release line until
26
- # Typer removes that dependency on deprecated APIs.
27
- click = ">=8.0,<8.5"
28
- sentry-sdk = "^2.42.1"
29
- update-checker = "^0.18.0"
30
- doit = "^0.36.0"
31
- textual = "^6.8.0"
23
+ typer = ">=0.24.2"
24
+ sentry-sdk = ">=2.42.1"
25
+ update-checker = "^1.0.0"
26
+ doit = ">=0.36.0"
32
27
  jinja2 = ">=3.1,<4.0"
33
28
  pygal = ">=3.0,<4.0"
34
- keyring = ">=25.6,<26.0"
29
+ keyring = ">=25.6"
30
+ # 0.1.7 added make_file_url()/path_from_file_url(); starbash.url re-exports them.
31
+ toml-repo = "^0.1.7"
35
32
 
36
33
  # Note: to use graxpert from the local tree rather than pypi use:
37
34
  # graxpert = {path = "GraXpert", extras = ["cpuonly"], develop = true}
@@ -39,24 +36,32 @@ graxpert = {version = "^3.2.0a2", extras = ["cpuonly"]}
39
36
 
40
37
  # The following two libs are only needed for the (temporary) experiment of running siril scripts directly...
41
38
  # experimenting with siril-scripts/processing/VeraLux_HyperMetric_Stretch.py
39
+ # pyqt6 is used *only* by those out-of-process siril scripts; Starbash itself never imports it,
40
+ # so it can safely coexist with PySide6 (which the GUI *does* import).
42
41
  numpy = "2.2.6"
43
42
  pyqt6 = "^6.10.1"
44
- toml-repo = "^0.1.6"
43
+ # The desktop GUI (`sb gui`) binding. A normal dependency: the GUI is a first-class
44
+ # way to drive Starbash. "Optional" only means users may keep using the CLI instead.
45
+ pyside6 = "^6.11.2"
45
46
 
46
47
  [tool.poetry.group.dev.dependencies]
47
48
  pytest = ">=8.4.2,<10.0.0"
48
- pytest-cov = "^6.0.0"
49
- pytest-xdist = "^3.8.0"
50
- pre-commit = "^4.3.0"
51
- ruff = "^0.14.4"
52
- basedpyright = "^1.33.0"
53
- textual-dev = "^1.8.0"
49
+ pytest-cov = ">=6.0.0"
50
+ pytest-xdist = ">=3.8.0"
51
+ pre-commit = ">=4.3.0"
52
+ ruff = ">=0.14.4"
53
+ basedpyright = ">=1.33.0"
54
+ # Qt widget testing for the `sb gui` desktop app (drives real widgets offscreen).
55
+ pytest-qt = ">=4.4.0"
54
56
 
55
57
  # Adds the `graph` command to doit (GraphViz output) for `sb process doit graph`.
56
- doit-graph = "^0.3.0"
58
+ doit-graph = ">=0.3.0"
57
59
 
58
60
  # We don't currently use this text based sql client
59
61
  # harlequin = "^2.4.1"
62
+ library-skills = "^0.0.19"
63
+ semble = {version = "^0.6.0", extras = ["mcp"]}
64
+ imageio-ffmpeg = "^0.6.0"
60
65
 
61
66
  [tool.poetry.scripts]
62
67
  starbash = "starbash.main:app"
@@ -66,14 +71,23 @@ sb = "starbash.main:app" # std alias for convenience
66
71
  markers = [
67
72
  "slow: marks tests as slow (deselect with '-m \"not slow\"')",
68
73
  "integration: marks tests as integration tests requiring /test-data (deselect with '-m \"not integration\"')",
74
+ "gui: marks tests that build real Qt widgets (offscreen; deselect with '-m \"not gui\"')",
75
+ "gui_integration: marks the scripted gui_integration movie runs (needs ffmpeg + /test-data; deselect with '-m \"not gui_integration\"')",
69
76
  ]
70
77
  # -n auto enables parallel test execution plugin
71
- # By default, exclude slow and integration tests
78
+ # By default we exclude only slow and integration tests. GUI tests DO run: PySide6
79
+ # ships as a normal dependency, and tests/conftest.py forces QT_QPA_PLATFORM=offscreen
80
+ # so they work headless. If Qt cannot start at all on a machine, they skip cleanly.
72
81
  # --tb=line shows minimal tracebacks (just one line per failure)
73
82
  # --tb=short shows shorter tracebacks
74
83
  # -rfE shows summary of failed and error test outcomes at end (not passed)
75
84
  # --ignore excludes third-party test directories
76
- addopts = "-m 'not slow and not integration' -n auto --tb=short -rfE --ignore=GraXpert --ignore=textual --ignore=starbash-recipes"
85
+ addopts = "-m 'not slow and not integration and not gui_integration' -n auto --tb=short -rfE --ignore=GraXpert --ignore=starbash-recipes"
86
+
87
+ # Note: the `gui_integration` movies are deselected by default on purpose: they drive a
88
+ # whole user journey (setup wizard -> reindex -> processing run) and take minutes, so they
89
+ # run on demand - `just test-integration-gui`, or `pytest -m gui_integration -n 0`.
90
+ # The marker name uses an underscore so it is usable as `pytest.mark.gui_integration`.
77
91
 
78
92
  # Note: To limit captured output further, you can:
79
93
  # 1. Use --tb=line for minimal tracebacks
@@ -119,9 +133,7 @@ exclude = [
119
133
  "starbash-recipes/**",
120
134
  "test-data/**",
121
135
  "GraXpert/**",
122
- "textual/**",
123
- "bin/**",
124
- "*.py" # Exclude root-level Python files
136
+ "bin/**"
125
137
  ]
126
138
 
127
139
  [tool.ruff.lint]
@@ -160,7 +172,8 @@ ignore = [
160
172
 
161
173
  [tool.basedpyright]
162
174
  # Enable import validation to catch runtime import errors at analysis time
163
- include = ["src"]
175
+ # Type-check both the package and the test suite.
176
+ include = ["src", "tests"]
164
177
  exclude = ["**/__pycache__", ".venv", "build", "dist"]
165
178
  extraPaths = ["GraXpert", "toml-repo"] # Include local sources for type checking
166
179
  typeCheckingMode = "standard" # Standard type checking: catches type errors, missing annotations, etc.
@@ -14,6 +14,15 @@ from starbash.check_version import is_connected
14
14
  # Default to no analytics/auto crash reports
15
15
  analytics_allowed = False
16
16
 
17
+ # Canonical defaults for the user's analytics preferences. These mirror the
18
+ # comments in ``templates/userconfig.toml`` and are the source of truth for every
19
+ # front end: the core (``app.py``), the CLI setup (``commands/user.py``) and the
20
+ # GUI settings page / first-run wizard all read the preference through
21
+ # ``analytics_enabled`` / ``analytics_include_user`` below, so an unset
22
+ # preference is treated identically everywhere.
23
+ DEFAULT_ANALYTICS_ENABLED = True
24
+ DEFAULT_ANALYTICS_INCLUDE_USER = False
25
+
17
26
  __all__ = [
18
27
  "analytics_setup",
19
28
  "analytics_shutdown",
@@ -22,9 +31,31 @@ __all__ = [
22
31
  "analytics_exception",
23
32
  "is_development_environment",
24
33
  "NopAnalytics",
34
+ "DEFAULT_ANALYTICS_ENABLED",
35
+ "DEFAULT_ANALYTICS_INCLUDE_USER",
36
+ "analytics_enabled",
37
+ "analytics_include_user",
25
38
  ]
26
39
 
27
40
 
41
+ def analytics_enabled(repo: Any) -> bool:
42
+ """Return whether the user has opted into analytics/crash reports.
43
+
44
+ Args:
45
+ repo: the user preferences repo (or any object with a ``get(key, default)``).
46
+ """
47
+ return bool(repo.get("analytics.enabled", DEFAULT_ANALYTICS_ENABLED))
48
+
49
+
50
+ def analytics_include_user(repo: Any) -> bool:
51
+ """Return whether the user's email may be attached to reports.
52
+
53
+ Args:
54
+ repo: the user preferences repo (or any object with a ``get(key, default)``).
55
+ """
56
+ return bool(repo.get("analytics.include_user", DEFAULT_ANALYTICS_INCLUDE_USER))
57
+
58
+
28
59
  def analytics_setup(allowed: bool = False, user_email: str | None = None) -> None:
29
60
  global analytics_allowed
30
61
  analytics_allowed = allowed
@@ -11,11 +11,10 @@ from typing import Any
11
11
  import rich.console
12
12
  import typer
13
13
  from rich.logging import RichHandler
14
- from rich.progress import track
15
14
  from toml_repo import Repo, RepoManager, get_config_suffix
16
15
 
17
16
  import starbash
18
- from starbash import doit_types
17
+ from starbash import doit_types, events
19
18
  from starbash.aliases import (
20
19
  Aliases,
21
20
  get_aliases,
@@ -24,7 +23,9 @@ from starbash.aliases import (
24
23
  )
25
24
  from starbash.analytics import (
26
25
  NopAnalytics,
26
+ analytics_enabled,
27
27
  analytics_exception,
28
+ analytics_include_user,
28
29
  analytics_setup,
29
30
  analytics_shutdown,
30
31
  analytics_start_transaction,
@@ -47,6 +48,7 @@ from starbash.score import ScoredCandidate, score_candidates
47
48
  from starbash.selection import Selection, build_search_conditions
48
49
  from starbash.toml import toml_from_template
49
50
  from starbash.tool import init_tools
51
+ from starbash.url import make_file_url
50
52
  from starbash.windows import windows_init
51
53
 
52
54
  critical_keys = [Database.DATE_OBS_KEY, Database.IMAGETYP_KEY]
@@ -131,13 +133,13 @@ def remap_expected_errors(exc: BaseException | None) -> BaseException | None:
131
133
  return None
132
134
 
133
135
  # Some OSErrors definitely don't indicate a code level bug...
134
- expected_errors = [ "Read-only file system", "No space left on device" ]
136
+ expected_errors = ["Read-only file system", "No space left on device"]
135
137
  # Update: I give up, keep seeing misc reports from field. I think all OSErrors should be considered
136
138
  # 'not a bug' until proven otherwise.
137
139
 
138
140
  if isinstance(exc, OperationalError):
139
141
  return NonSoftwareError(f"[red]Database IO error:[/red] {exc}")
140
- elif isinstance(exc, OSError): # Was FileNotFoundError
142
+ elif isinstance(exc, OSError): # Was FileNotFoundError
141
143
  return NonSoftwareError(f"[red]OS error:[/red] {exc}")
142
144
  elif isinstance(exc, OSError):
143
145
  exc_str = str(exc)
@@ -215,7 +217,7 @@ class Starbash:
215
217
  submodule_path = Path(__file__).parent.parent.parent / "starbash-recipes"
216
218
  if submodule_path.exists():
217
219
  logging.info(f"Using local recipes from {submodule_path}")
218
- self.repo_manager.add_repo(f"file://{submodule_path}")
220
+ self.repo_manager.add_repo(make_file_url(submodule_path))
219
221
  return True
220
222
  return False
221
223
 
@@ -225,7 +227,7 @@ class Starbash:
225
227
  self.repo_manager.add_repo("pkg://defaults")
226
228
 
227
229
  # Add user prefs as a repo
228
- self.user_repo = self.repo_manager.add_repo("file://" + str(create_user()))
230
+ self.user_repo = self.repo_manager.add_repo(make_file_url(create_user()))
229
231
 
230
232
  # We always need at least one set of recipes. If the user hasn't specified one use the default.
231
233
  if self.repo_manager.get_repo_by_kind("std-recipe") is None:
@@ -251,8 +253,8 @@ class Starbash:
251
253
 
252
254
  def _init_analytics(self, cmd: str) -> None:
253
255
  self.analytics = NopAnalytics()
254
- if self.user_repo.get("analytics.enabled", True):
255
- include_user = self.user_repo.get("analytics.include_user", False)
256
+ if analytics_enabled(self.user_repo):
257
+ include_user = analytics_include_user(self.user_repo)
256
258
  user_email = self.user_repo.get("user.email", None) if include_user else None
257
259
  if user_email is not None:
258
260
  user_email = str(user_email)
@@ -302,7 +304,12 @@ class Starbash:
302
304
  def __enter__(self) -> "Starbash":
303
305
  return self
304
306
 
305
- def __exit__(self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None) -> bool:
307
+ def __exit__(
308
+ self,
309
+ exc_type: type[BaseException] | None,
310
+ exc: BaseException | None,
311
+ tb: types.TracebackType | None,
312
+ ) -> bool:
306
313
  from starbash import _is_test_env
307
314
  from starbash.exception import UserHandledError
308
315
 
@@ -326,12 +333,18 @@ class Starbash:
326
333
  self.close()
327
334
  if not handled:
328
335
  # Show the full exception for developers
329
- starbash.console.print_exception(show_locals=False) # Locals are too verbose
336
+ starbash.console.print_exception(show_locals=False) # Locals are too verbose
330
337
 
331
- # In test environments, let exceptions propagate naturally for better test diagnostics
332
- if not _is_test_env:
333
- # But in any case, make our app exit with an error code
334
- raise typer.Exit(code=1)
338
+ # In test environments, let exceptions propagate naturally for better
339
+ # test diagnostics. Return False *explicitly*: ``analytics_exception``
340
+ # is usually mocked in tests, and a mock returns a truthy value which
341
+ # would otherwise be returned from ``__exit__`` and silently suppress
342
+ # the exception (hiding real failures).
343
+ if _is_test_env:
344
+ return False
345
+
346
+ # But in any case, make our app exit with an error code
347
+ raise typer.Exit(code=1)
335
348
  else:
336
349
  self.close()
337
350
 
@@ -366,9 +379,14 @@ class Starbash:
366
379
  if filter:
367
380
  new[get_column_name(Database.FILTER_KEY)] = filter
368
381
 
369
- telescop = header.get(Database.TELESCOP_KEY)
370
- if telescop:
371
- new[get_column_name(Database.TELESCOP_KEY)] = telescop
382
+ # The sessions table declares `telescop` NOT NULL, but a frame without a
383
+ # TELESCOP header is legitimate - `_extend_image_header` even falls back
384
+ # to CREATOR for such files. Store "" ("unknown") rather than omitting
385
+ # the key: omitting it made upsert_session insert NULL and abort the whole
386
+ # repo scan with an IntegrityError. `get_session` treats an empty
387
+ # telescope as "match any", so these frames still join the same rig's
388
+ # existing session when there is one.
389
+ new[get_column_name(Database.TELESCOP_KEY)] = header.get(Database.TELESCOP_KEY) or ""
372
390
 
373
391
  obj = header.get(Database.OBJECT_KEY)
374
392
  if obj:
@@ -551,7 +569,10 @@ class Starbash:
551
569
  # filtered_images.append(img)
552
570
  # images = filtered_images
553
571
 
554
- return images
572
+ # Reconstruct absolute paths, exactly like get_session_images() does. Master
573
+ # rows only carry a repo-relative ``path``, so without this callers (e.g. the
574
+ # GUI's master preview) have no way to open the file.
575
+ return [self._add_image_abspath(image) for image in images]
555
576
 
556
577
  def add_filter_not_masters(self, conditions: list[SearchCondition]) -> None:
557
578
  """Add conditions to filter out master and processed repos from image searches."""
@@ -627,36 +648,69 @@ class Starbash:
627
648
  # Reconstruct absolute paths for all images
628
649
  return [self._add_image_abspath(img) for img in filtered_images]
629
650
 
630
- def remove_repo_ref(self, url: str) -> None:
651
+ def _find_user_repo_ref(self, url: str) -> dict | None:
631
652
  """
632
- Remove a repository reference from the user configuration.
653
+ Find the user configuration ``[[repo-ref]]`` entry matching a repository.
633
654
 
634
655
  Args:
635
- url: The repository URL to remove (e.g., 'file:///path/to/repo')
656
+ url: The repository URL or directory to look for. Both are accepted,
657
+ because the CLI passes whatever the user typed on the command line
658
+ (a number, a path or a URL) and the GUI passes the URL from its
659
+ repository table. (e.g., 'file:///path/to/repo')
636
660
 
637
- Raises:
638
- ValueError: If the repository URL is not found in user configuration
661
+ Returns:
662
+ The matching repo-ref entry, or None if the URL is not user configured.
639
663
  """
640
- self.db.remove_repo(url)
664
+ for ref in self.user_repo.config.get("repo-ref", []):
665
+ ref_dir = ref.get("dir", "")
666
+ if not ref_dir:
667
+ continue
668
+ # ``dir`` is always recorded as a plain filesystem path
669
+ # (Repo.add_repo_ref writes str(path)), so compare paths directly and
670
+ # fall back to the canonical URL -- that is the spelling Starbash
671
+ # stores elsewhere, and on Windows it differs far more than the
672
+ # "file://" prefix (file:///C:/data vs file://C:\data).
673
+ if ref_dir == url or make_file_url(ref_dir) == url:
674
+ return ref
675
+ return None
641
676
 
642
- # Get the repo-ref list from user config
643
- repo_refs: list = self.user_repo.config.get("repo-ref", [])
677
+ def is_repo_removable(self, url: str) -> bool:
678
+ """
679
+ Whether remove_repo_ref() would be able to remove a repository.
644
680
 
645
- # Find and remove the matching repo-ref
646
- found = False
647
- refs_copy = [r for r in repo_refs] # Make a copy to iterate
648
- for ref in refs_copy:
649
- ref_dir = ref.get("dir", "")
650
- # Match by converting to file:// URL format if needed
651
- if ref_dir == url or f"file://{ref_dir}" == url:
652
- repo_refs.remove(ref)
681
+ Only repositories the user added are recorded in the user configuration;
682
+ the ones Starbash manages itself (``pkg://defaults``, the recipes checkout
683
+ and the preferences repo) have no entry there and therefore cannot be
684
+ removed individually.
685
+
686
+ Args:
687
+ url: The repository URL to check (e.g., 'file:///path/to/repo')
688
+
689
+ Returns:
690
+ True if the repository is recorded in the user configuration.
691
+ """
692
+ return self._find_user_repo_ref(url) is not None
693
+
694
+ def remove_repo_ref(self, url: str) -> None:
695
+ """
696
+ Remove a repository reference from the user configuration.
653
697
 
654
- found = True
655
- break
698
+ Args:
699
+ url: The repository URL to remove (e.g., 'file:///path/to/repo')
656
700
 
657
- if not found:
701
+ Raises:
702
+ UserHandledError: If the repository URL is not found in user configuration
703
+ """
704
+ ref = self._find_user_repo_ref(url)
705
+ if ref is None:
706
+ # Check before touching the database: for a repository we refuse to
707
+ # remove (the recipes checkout, pkg://defaults, ...) dropping its
708
+ # indexed rows would only force a pointless re-index later.
658
709
  raise UserHandledError(f"Repository '{url}' not found in user configuration.")
659
710
 
711
+ self.db.remove_repo(url)
712
+ self.user_repo.config.get("repo-ref", []).remove(ref)
713
+
660
714
  # Write the updated config
661
715
  self.user_repo.write_config()
662
716
 
@@ -757,7 +811,21 @@ class Starbash:
757
811
  self._add_session(headers)
758
812
 
759
813
  def reindex_repo(self, repo: Repo, subdir: str | None = None) -> None:
760
- """Reindex all repositories managed by the RepoManager."""
814
+ """Scan one repo's files into the database.
815
+
816
+ Progress is *reported*, never drawn: the scan publishes
817
+ ``reindex.progress`` / ``reindex.finished`` per repo (see
818
+ :mod:`starbash.events`) and the front ends render those -- the CLI's
819
+ :class:`~starbash.ui.cli.ReindexView`, a processing run's own live view,
820
+ or the GUI's Repositories page. A ``rich`` bar drawn from here would be a
821
+ *second* display racing the run's live view on the terminal, which is the
822
+ problem ``doc/plans/cli-live-display.md`` already removed from
823
+ ``starbash.tool``.
824
+
825
+ Args:
826
+ repo: the repo to scan.
827
+ subdir: scan only this subdirectory of the repo (for debugging).
828
+ """
761
829
 
762
830
  # make sure this new repo is listed in the repos table
763
831
  self.repo_db_update() # not really ideal, a more optimal version would just add the new repo
@@ -774,10 +842,19 @@ class Starbash:
774
842
 
775
843
  # Find all FITS files under this repo path
776
844
  all_files = list(path.rglob("*.fit")) + list(path.rglob("*.fits"))
777
- for f in track(
778
- all_files,
779
- description=f"Indexing {repo.url}...",
780
- ):
845
+ total_files = len(all_files)
846
+ events.publish(
847
+ events.EVENT_REINDEX_PROGRESS,
848
+ {"repo": repo.url, "done": 0, "total": total_files},
849
+ )
850
+ for index, f in enumerate(all_files, start=1):
851
+ # Throttle progress events: a repo can hold tens of thousands of
852
+ # frames and we don't want to flood the event bus / GUI.
853
+ if index % 25 == 0 or index == total_files:
854
+ events.publish(
855
+ events.EVENT_REINDEX_PROGRESS,
856
+ {"repo": repo.url, "done": index, "total": total_files, "file": str(f)},
857
+ )
781
858
  if ".sbignore" in str(f):
782
859
  logging.warning('Skipping "%s": path contains ".sbignore".', f)
783
860
  continue
@@ -794,11 +871,19 @@ class Starbash:
794
871
  except OSError as e:
795
872
  logging.error(f'Skipping "{f}" due to: [red]{e}[/red]')
796
873
 
874
+ events.publish(
875
+ events.EVENT_REINDEX_FINISHED,
876
+ {"repo": repo.url, "indexed": total_files},
877
+ )
878
+
797
879
  def reindex_repos(self) -> None:
798
- """Reindex all repositories managed by the RepoManager."""
880
+ """Reindex all repositories managed by the RepoManager.
881
+
882
+ Reports progress through the event bus only -- see :meth:`reindex_repo`.
883
+ """
799
884
  logging.debug("Reindexing all repositories...")
800
885
 
801
- for repo in track(self.repo_manager.repos, description="Reindexing repos..."):
886
+ for repo in self.repo_manager.repos:
802
887
  self.reindex_repo(repo)
803
888
 
804
889
  def get_recipes(self) -> list[Repo]:
@@ -0,0 +1,49 @@
1
+ <?xml version="1.0" encoding="iso-8859-1"?>
2
+ <!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
3
+ <svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
4
+ viewBox="0 0 241.168 241.168" xml:space="preserve">
5
+ <g>
6
+ <g>
7
+ <g id="XMLID_28_">
8
+ <g>
9
+ <rect x="114.406" y="167.755" style="fill:#EAEEEF;" width="27.663" height="2.643"/>
10
+ <path style="fill:#EAEEEF;" d="M128.733,144.086c1.352,0,2.463,1.081,2.463,2.403c0,1.352-1.111,2.433-2.463,2.433
11
+ c-1.382,0-2.493-1.081-2.493-2.433C126.24,145.167,127.351,144.086,128.733,144.086z"/>
12
+ <polygon style="fill:#EAEEEF;" points="45.142,119.997 47.725,124.502 43.73,126.695 41.117,122.219 "/>
13
+ <polygon style="fill:#EAEEEF;" points="144.262,38.929 169.222,82.061 88.364,121.469 68.059,86.386 "/>
14
+ <polygon style="fill:#505967;" points="22.945,124.773 32.106,140.632 27.51,143.155 18.349,127.326 "/>
15
+ <path style="fill:#2D213F;" d="M236.744,17.002c9.281,16.069,3.484,36.554-12.946,45.625l-4.596,2.553l0.33,0.541
16
+ c1.051,1.862,1.292,4.055,0.661,6.067c-0.571,1.892-1.892,3.514-3.664,4.475c-0.12,0.06-0.24,0.12-0.36,0.18l-79.056,38.537
17
+ v15.739c5.857,3.004,9.882,8.891,9.882,15.769c0,2.042-0.421,3.995-1.081,5.797h4.055c4.355,0,7.9,3.484,7.9,7.749v14.898
18
+ l53.855,52.684c3.094,3.004,3.094,7.9,0,10.933c-1.532,1.502-3.574,2.253-5.587,2.253c-2.012,0-4.055-0.751-5.587-2.253
19
+ l-53.855-52.684h-10.062v47.578c0,4.265-3.544,7.719-7.9,7.719c-4.385,0-7.9-3.454-7.9-7.719v-47.578H109.78l-52.654,51.452
20
+ c-1.532,1.532-3.544,2.283-5.587,2.283c-2.012,0-4.025-0.751-5.587-2.283c-3.064-3.004-3.064-7.9,0-10.903l52.654-51.482
21
+ v-14.898c0-4.265,3.514-7.749,7.9-7.749h5.016c-0.661-1.802-1.081-3.755-1.081-5.797c0-7.269,4.475-13.516,10.873-16.28v-7.539
22
+ l-51.512,25.11c-3.755,1.832-8.35,0.481-10.393-3.124l-3.905-6.728l-5.346,2.974c0.901,3.214-0.601,6.788-3.845,8.59
23
+ l-18.352,10.152c-3.785,2.103-8.53,0.961-10.543-2.523l-16.49-28.535c-2.042-3.484-0.601-8.05,3.214-10.152l18.322-10.122
24
+ c3.274-1.802,7.209-1.231,9.552,1.201l5.346-2.974l-4.535-7.837c-2.073-3.574-0.931-8.11,2.613-10.333L173.397,2.465
25
+ c1.832-1.111,4.055-1.472,6.157-0.961c2.103,0.511,3.905,1.832,4.956,3.694l0.991,1.682l4.566-2.553
26
+ C206.527-4.744,227.432,0.933,236.744,17.002z M216.018,49.171c8.831-4.896,11.954-15.919,6.968-24.57
27
+ c-5.016-8.65-16.28-11.714-25.14-6.818l-4.566,2.553l18.142,31.388L216.018,49.171z M201.661,66.262l-26.792-46.376
28
+ l-17.241,10.753l25.741,44.544L201.661,66.262z M169.222,82.061l-24.96-43.132L68.059,86.386l20.305,35.082L169.222,82.061z
29
+ M142.069,170.398v-2.643h-27.663v2.643H142.069z M131.196,146.489c0-1.322-1.111-2.403-2.463-2.403
30
+ c-1.382,0-2.493,1.081-2.493,2.403c0,1.352,1.111,2.433,2.493,2.433C130.084,148.922,131.196,147.84,131.196,146.489z
31
+ M74.187,128.377L54.723,94.706l-4.445,2.763l19.193,33.22L74.187,128.377z M47.725,124.502l-2.583-4.505l-4.025,2.223
32
+ l2.613,4.475L47.725,124.502z M32.106,140.632l-9.161-15.859l-4.596,2.553l9.161,15.829L32.106,140.632z"/>
33
+ <path style="fill:#FEDE94;" d="M222.987,24.601c4.986,8.65,1.862,19.674-6.968,24.57l-4.596,2.553l-18.142-31.388l4.566-2.553
34
+ C206.707,12.887,217.971,15.951,222.987,24.601z"/>
35
+ <polygon style="fill:#505967;" points="174.869,19.886 201.661,66.262 183.369,75.183 157.628,30.639 "/>
36
+ <polygon style="fill:#505967;" points="54.723,94.706 74.187,128.377 69.471,130.69 50.278,97.47 "/>
37
+ </g>
38
+ </g>
39
+ <polygon style="fill:#454D5B;" points="183.369,75.183 201.661,66.262 192.857,51.024 175.304,61.228 "/>
40
+ <polygon style="fill:#C5CBCF;" points="88.364,121.469 169.222,82.061 161.731,69.118 84.159,114.203 "/>
41
+ <polygon style="fill:#454D5B;" points="69.471,130.69 74.187,128.377 70.561,122.105 66.032,124.74 "/>
42
+ <polygon style="fill:#454D5B;" points="174.869,19.886 157.628,30.639 165.305,43.927 182.981,33.928 "/>
43
+ <polygon style="fill:#C5CBCF;" points="144.262,38.929 68.059,86.386 73.535,95.848 151.63,51.664 "/>
44
+ <polygon style="fill:#454D5B;" points="54.723,94.706 50.278,97.47 55.3,106.165 59.856,103.588 "/>
45
+ <path style="fill:#F2CE75;" d="M216.018,49.171c8.831-4.896,11.954-15.919,6.968-24.57c-1.114-1.922-2.55-3.55-4.187-4.893
46
+ c3.664,8.641,2.454,21.996-6.845,31.721L216.018,49.171z"/>
47
+ </g>
48
+ </g>
49
+ </svg>