pyobs-gui 2.6.3__tar.gz → 2.7.0__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 (203) hide show
  1. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/CHANGELOG.md +15 -0
  2. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/PKG-INFO +2 -2
  3. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/structuredconfigwidget.py +49 -14
  4. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyproject.toml +2 -2
  5. pyobs_gui-2.7.0/specs/2026-10-14-typed-config-choices.md +92 -0
  6. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/index.md +6 -0
  7. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_structuredconfigwidget.py +117 -0
  8. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/uv.lock +303 -226
  9. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.claude/skills/verify/SKILL.md +0 -0
  10. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.github/dependabot.yml +0 -0
  11. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.github/workflows/dependabot-automerge.yml +0 -0
  12. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.github/workflows/pypi.yml +0 -0
  13. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.github/workflows/pyrefly.yml +0 -0
  14. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.github/workflows/ruff.yml +0 -0
  15. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.gitignore +0 -0
  16. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.pre-commit-config.yaml +0 -0
  17. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/.readthedocs.yml +0 -0
  18. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/CLAUDE.md +0 -0
  19. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/CONTRIBUTING.md +0 -0
  20. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/LICENSE +0 -0
  21. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/README.md +0 -0
  22. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/bin/mypyuic5 +0 -0
  23. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/docs/Makefile +0 -0
  24. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/docs/make.bat +0 -0
  25. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/docs/requirements.txt +0 -0
  26. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/docs/source/_static/pyobs.gif +0 -0
  27. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/docs/source/conf.py +0 -0
  28. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/docs/source/index.rst +0 -0
  29. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/mypy.ini +0 -0
  30. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/__init__.py +0 -0
  31. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/__main__.py +0 -0
  32. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
  33. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/accounts.py +0 -0
  34. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/acquisitionwidget.py +0 -0
  35. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/autofocuswidget.py +0 -0
  36. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/autoguidingwidget.py +0 -0
  37. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/base.py +0 -0
  38. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/camerawidget.py +0 -0
  39. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/commandinputwidget.py +0 -0
  40. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/compassmovewidget.py +0 -0
  41. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/coolingwidget.py +0 -0
  42. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/datadisplaywidget.py +0 -0
  43. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/eventswidget.py +0 -0
  44. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/filterwidget.py +0 -0
  45. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/fitsheaderswidget.py +0 -0
  46. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/focuswidget.py +0 -0
  47. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/gui.py +0 -0
  48. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/guisignals.py +0 -0
  49. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/livestream.py +0 -0
  50. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/login.py +0 -0
  51. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/loginwindow.py +0 -0
  52. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/logmodel.py +0 -0
  53. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/mainwindow.py +0 -0
  54. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/modewidget.py +0 -0
  55. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/modifiedmixin.py +0 -0
  56. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/modulegui.py +0 -0
  57. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/movestack.py +0 -0
  58. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/notificationmanager.py +0 -0
  59. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/notifications.py +0 -0
  60. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/notifier.py +0 -0
  61. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/notifier_backend.py +0 -0
  62. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/nowheelfilter.py +0 -0
  63. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/__init__.py +0 -0
  64. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  65. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  66. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  67. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  68. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  69. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  70. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/camerawidget.ui +0 -0
  71. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  72. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  73. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/compassmovewidget.py +0 -0
  74. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  75. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  76. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/compile.sh +0 -0
  77. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/coolingwidget.ui +0 -0
  78. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  79. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  80. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  81. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  82. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/designer.sh +0 -0
  83. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/eventswidget.ui +0 -0
  84. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  85. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/filterwidget.ui +0 -0
  86. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  87. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  88. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  89. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/focuswidget.ui +0 -0
  90. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  91. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/mainwindow.ui +0 -0
  92. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  93. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/modewidget.ui +0 -0
  94. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/modewidget_ui.py +0 -0
  95. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  96. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  97. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  98. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  99. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  100. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  101. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  102. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  103. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  104. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  105. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  106. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  107. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources.qrc +0 -0
  108. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/resources_rc.py +0 -0
  109. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/roboticwidget.ui +0 -0
  110. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
  111. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/roofwidget.ui +0 -0
  112. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  113. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/schedulewidget.ui +0 -0
  114. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
  115. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/shellwidget.ui +0 -0
  116. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  117. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  118. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  119. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/telescopewidget.ui +0 -0
  120. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  121. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  122. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  123. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  124. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  125. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/videograbwidget.ui +0 -0
  126. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/videograbwidget_ui.py +0 -0
  127. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/videowidget.ui +0 -0
  128. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/videowidget_ui.py +0 -0
  129. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/weatherwidget.ui +0 -0
  130. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  131. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/roboticwidget.py +0 -0
  132. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/roofwidget.py +0 -0
  133. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/schedulewidget.py +0 -0
  134. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/settings.py +0 -0
  135. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/settingsdialog.py +0 -0
  136. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/shellwidget.py +0 -0
  137. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/spectrographwidget.py +0 -0
  138. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/statuswidget.py +0 -0
  139. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/telescopewidget.py +0 -0
  140. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/temperaturesplotwidget.py +0 -0
  141. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/temperatureswidget.py +0 -0
  142. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/utils.py +0 -0
  143. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/videograbwidget.py +0 -0
  144. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/videowidget.py +0 -0
  145. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/watchedlabel.py +0 -0
  146. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/weatherwidget.py +0 -0
  147. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pyobs_gui/windows_identity.py +0 -0
  148. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/pysidedeploy.spec +0 -0
  149. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  150. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
  151. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +0 -0
  152. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-08-28-structuredconfig-widget.md +0 -0
  153. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-08-31-irobotic-widgets.md +0 -0
  154. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-09-01-gui-video-widget-split.md +0 -0
  155. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-09-14-fitswidget-toolbar-overflow.md +0 -0
  156. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-09-14-stacked-widget-scroll-fallback.md +0 -0
  157. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-09-29-live-view-mjpeg-raw.md +0 -0
  158. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-10-04-desktop-notifications.md +0 -0
  159. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/2026-10-04-settings-dialog.md +0 -0
  160. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/plans/2026-10-04-desktop-notifications.md +0 -0
  161. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/plans/2026-10-04-settings-dialog.md +0 -0
  162. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/specs/plans/index.md +0 -0
  163. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/spike_standalone/main.py +0 -0
  164. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/spike_standalone/pysidedeploy.spec +0 -0
  165. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/acquisition.yaml +0 -0
  166. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/autofocus.yaml +0 -0
  167. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/camera.yaml +0 -0
  168. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/full.yaml +0 -0
  169. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/guiding.yaml +0 -0
  170. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/mode.yaml +0 -0
  171. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/robotic.yaml +0 -0
  172. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/roof.yaml +0 -0
  173. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/spectrograph.yaml +0 -0
  174. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/structuredconfig.yaml +0 -0
  175. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/telescope.yaml +0 -0
  176. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/telescope_acl.yaml +0 -0
  177. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/telescope_acl_denied.yaml +0 -0
  178. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/test/video.yaml +0 -0
  179. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/__init__.py +0 -0
  180. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/conftest.py +0 -0
  181. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_accounts.py +0 -0
  182. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_basewidget_discard.py +0 -0
  183. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_camerawidget.py +0 -0
  184. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_gui_notifications.py +0 -0
  185. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_import_order.py +0 -0
  186. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_livestream.py +0 -0
  187. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_loginwindow.py +0 -0
  188. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_mainwindow_startup.py +0 -0
  189. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_missing_vfs_root.py +0 -0
  190. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_modewidget.py +0 -0
  191. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_multiwidget_pages.py +0 -0
  192. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_notificationmanager.py +0 -0
  193. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_notifications.py +0 -0
  194. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_notifier_backend.py +0 -0
  195. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_roboticwidget.py +0 -0
  196. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_schedulewidget.py +0 -0
  197. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_settings.py +0 -0
  198. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_settingsdialog.py +0 -0
  199. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_spectrographwidget.py +0 -0
  200. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_video_refresh.py +0 -0
  201. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_videograbwidget.py +0 -0
  202. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_videowidget.py +0 -0
  203. {pyobs_gui-2.6.3 → pyobs_gui-2.7.0}/tests/test_windows_identity.py +0 -0
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  Entries for releases before this file existed were generated from commit subjects.
9
9
 
10
+ ## [2.7.0] - 2026-10-15
11
+
12
+ ### Added
13
+
14
+ - Display structured-configuration choice labels while retaining the original typed
15
+ values; preserve unknown stored choices for explicit correction (#211, #218).
16
+
17
+ ### Fixed
18
+
19
+ - Distinguish boolean and numeric choices when tracking configuration edits (#218).
20
+
21
+ ### Changed
22
+
23
+ - Require pyobs-core 2.18.0 or newer for typed configuration-choice capabilities.
24
+
10
25
  ## [2.6.1] - 2026-10-04
11
26
 
12
27
  - Require pyobs-core 2.14.0, which has the slimmed base dependencies
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-gui
3
- Version: 2.6.3
3
+ Version: 2.7.0
4
4
  Summary: A remote GUI for pyobs
5
5
  Project-URL: Homepage, https://www.pyobs.org
6
6
  Project-URL: Documentation, https://docs.pyobs.org
@@ -19,7 +19,7 @@ Requires-Dist: keyring<26,>=25.7.0
19
19
  Requires-Dist: matplotlib<4,>=3.10.1
20
20
  Requires-Dist: pandas>=2.2.3
21
21
  Requires-Dist: pydantic<3,>=2.12.5
22
- Requires-Dist: pyobs-core<3,>=2.14.0
22
+ Requires-Dist: pyobs-core<3,>=2.18.0
23
23
  Requires-Dist: pyside6-essentials>=6.10.1
24
24
  Requires-Dist: pywin32>=312; sys_platform == 'win32'
25
25
  Requires-Dist: pyyaml<7,>=6.0.2
@@ -85,6 +85,53 @@ def _nested_set(children: Dict[str, _FieldEditor], value: ConfigValue) -> None:
85
85
  child.set_value(value[name])
86
86
 
87
87
 
88
+ def _choice_equal(left: ConfigValue, right: ConfigValue) -> bool:
89
+ if isinstance(left, dict) and isinstance(right, dict):
90
+ return left.keys() == right.keys() and all(_choice_equal(value, right[key]) for key, value in left.items())
91
+ if isinstance(left, list) and isinstance(right, list):
92
+ return len(left) == len(right) and all(_choice_equal(a, b) for a, b in zip(left, right, strict=True))
93
+ if type(left) in (int, float) and type(right) in (int, float):
94
+ return left == right
95
+ return type(left) is type(right) and left == right
96
+
97
+
98
+ def _build_enum_editor(schema: ConfigFieldSchema, on_change: Callable[[], None]) -> _FieldEditor:
99
+ combo = QtWidgets.QComboBox()
100
+ # Feature detection keeps older string-only core installations usable.
101
+ choices = getattr(schema, "choices", None)
102
+ values: list[ConfigValue] = (
103
+ [choice.value for choice in choices] if choices is not None else list(schema.options or [])
104
+ )
105
+ labels = [choice.label for choice in choices] if choices is not None else [str(value) for value in values]
106
+ combo.addItems(labels)
107
+ raw: list[ConfigValue] = [cast("ConfigValue", None)]
108
+
109
+ def set_value(value: ConfigValue) -> None:
110
+ raw[0] = value
111
+ if combo.count() > len(values):
112
+ combo.removeItem(len(values))
113
+ index = next((i for i, allowed in enumerate(values) if _choice_equal(value, allowed)), None)
114
+ if index is None:
115
+ index = len(values)
116
+ combo.addItem(f"(unknown value: {value!r})")
117
+ combo.setCurrentIndex(index)
118
+
119
+ def get_value() -> ConfigValue:
120
+ index = combo.currentIndex()
121
+ return values[index] if 0 <= index < len(values) else raw[0]
122
+
123
+ if schema.default is not None or not values:
124
+ set_value(cast("ConfigValue", schema.default))
125
+ combo.currentIndexChanged.connect(lambda _: on_change())
126
+ return _FieldEditor(
127
+ combo,
128
+ get_value,
129
+ set_value,
130
+ level=schema.level,
131
+ row_widget=_wrap_with_description(combo, schema.description),
132
+ )
133
+
134
+
88
135
  def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[], None]) -> _FieldEditor:
89
136
  """Build one field's editor from its schema (recursively, for a nested object)."""
90
137
 
@@ -146,19 +193,7 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
146
193
  )
147
194
 
148
195
  if schema.type == "enum":
149
- combo_box = QtWidgets.QComboBox()
150
- options = schema.options or []
151
- combo_box.addItems(options)
152
- if schema.default is not None and str(schema.default) in options:
153
- combo_box.setCurrentText(str(schema.default))
154
- combo_box.currentTextChanged.connect(lambda _: on_change())
155
- return _FieldEditor(
156
- combo_box,
157
- combo_box.currentText,
158
- lambda value: combo_box.setCurrentText(str(value)),
159
- level=schema.level,
160
- row_widget=_wrap_with_description(combo_box, schema.description),
161
- )
196
+ return _build_enum_editor(schema, on_change)
162
197
 
163
198
  if schema.type == "object" and schema.nested is not None:
164
199
  group_box = QtWidgets.QGroupBox(name)
@@ -325,7 +360,7 @@ class StructuredConfigWidget(BaseWidget):
325
360
  def _is_dirty(self) -> bool:
326
361
  if self._root is None or self._last_applied is None:
327
362
  return False
328
- return self._root.get_value() != self._last_applied
363
+ return not _choice_equal(self._root.get_value(), self._last_applied)
329
364
 
330
365
  def _update_buttons(self) -> None:
331
366
  dirty = self._is_dirty()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-gui"
3
- version = "2.6.3"
3
+ version = "2.7.0"
4
4
  description = "A remote GUI for pyobs"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11"
@@ -15,7 +15,7 @@ dependencies = [
15
15
  "astroquery>=0.4.10,<0.5",
16
16
  "QtAwesome>=1.4.0,<2",
17
17
  "qfitswidget>=1.2.0",
18
- "pyobs-core>=2.14.0,<3",
18
+ "pyobs-core>=2.18.0,<3",
19
19
  "pyside6-essentials>=6.10.1",
20
20
  "astroplan>=0.10.1",
21
21
  "astropy>=7.0.1,<8",
@@ -0,0 +1,92 @@
1
+ # Plan: preserve typed structured-config choices and show their labels
2
+
3
+ Status: implemented; release and deployment pending.
4
+ Repos: pyobs-gui, pyobs-core.
5
+ Issue: [#211](https://github.com/pyobs/pyobs-gui/issues/211).
6
+ Shared contract and rollout: [core plan](../../pyobs-core/specs/plans/2026-10-14-typed-config-choices.md).
7
+ Companion: [portal plan](../../pyobs-portal/specs/plans/2026-10-14-typed-config-choices.md).
8
+
9
+ ## Verified cause and intended behavior
10
+
11
+ `pyobs_gui/structuredconfigwidget.py::_build_editor` uses `QComboBox.addItems`,
12
+ `currentText`, and `setCurrentText` for enum choices. Its numeric spin boxes already
13
+ return numbers. Core currently stringifies Pydantic Literal values before the GUI
14
+ sees them, so a GUI-only cast is not sufficient.
15
+
16
+ With the coordinated core change, a synthetic choice `16416` can display
17
+ `Detector A` and submit integer `16416`. Labels come from capability metadata;
18
+ the widget remains generic and consumes no provider-specific label tables.
19
+
20
+ ## Implementation checklist
21
+
22
+ - [x] Feature-detect optional typed `choices` records on `ConfigFieldSchema`,
23
+ retaining compatibility with existing core versions and legacy string `options`.
24
+ - [x] Prefer `schema.choices` when present. Populate each combo entry with its display
25
+ label and keep its original scalar value in an authoritative Python-side list
26
+ indexed by combo index. Qt user data is also viable if tests establish exact
27
+ scalar preservation; do not rely on QVariant's implicit conversions.
28
+ - [x] Get the selected value from that typed collection, never `currentText`. Use
29
+ index-based change notifications so entries with identical labels remain distinct.
30
+ - [x] Set defaults/state/reset selection by scalar identity: bool differs from number,
31
+ strings differ from numbers, and numerical int/float equality follows the shared
32
+ JSON contract. Never match by display label or use `str(value)` as identity.
33
+ - [x] Preserve duplicate labels, `"1"` versus `1`, `True` versus `1`, and string codes
34
+ such as `"001"`. Do not globally parse numeric-looking strings.
35
+ - [x] Define invalid/missing selection behavior explicitly: if a received value is not
36
+ in the domain, preserve it with a visibly marked synthetic entry until the user
37
+ chooses a legal value. Never silently retain the previous selection or choose the
38
+ first entry. An empty domain needs a preserved raw value or a disabled editor;
39
+ getting index -1 must not accidentally return the last allowed option.
40
+ - [x] Keep default fallback only for genuinely absent values. A received `None` for a
41
+ non-null choice is a preserved invalid value, not a first-choice substitution.
42
+ General nullable editor support is outside this fix.
43
+ - [x] With `choices=None`, render legacy `options` as string-valued choices as before.
44
+ A numeric state value against a string-only legacy domain must remain visible
45
+ as unsupported/unknown rather than being guessed into a type. Document that
46
+ upgrading the producer is necessary for the numeric Literal fix.
47
+ - [x] Preserve descriptions, expert visibility, permissions, nested editors, apply/reset
48
+ flow and incoming-state dirty suppression. Test payloads through the existing
49
+ `_apply_config` proxy path. Avoid broad dirty-state or hidden-field refactoring.
50
+
51
+ ## Tests and acceptance
52
+
53
+ Extend `tests/test_structuredconfigwidget.py` (confirm existing test helpers before
54
+ adding fixtures), exercising real Qt widgets with the existing headless test setup:
55
+
56
+ - [x] Typed integer/float/bool/string choices retain exact Python types on initial
57
+ render, selection, state refresh, reset and apply. An unchanged numeric state
58
+ must not become dirty solely because a combo returns text.
59
+ - [x] Labels render correctly, including the Unicode choice label. Duplicate labels and
60
+ mixed typed values select by identity and trigger change handling by index.
61
+ - [x] Nested `main.detector` payload reaches the mocked `IStructuredConfig.set_config` as
62
+ an integer; rejection/server validation behavior remains intact.
63
+ - [x] Plain integer/float editors still return numbers; old string-only capabilities
64
+ still work. Partial/missing labels use raw display fallbacks from core.
65
+ - [x] Unknown value, None, empty options, invalid default, and invalid current index
66
+ do not silently overwrite configuration with the first or last option.
67
+ - [ ] Run the affected Qt suite and repository lint/type checks. Against candidate
68
+ installed core and model-provider versions, load a real representative configuration,
69
+ change a numeric choice and a numeric scalar, Apply, observe published state, then Reset.
70
+ Confirm no hardware operation beyond the explicitly authorized test environment.
71
+
72
+ ## Deployment constraints
73
+
74
+ For the full typed-choice fix, the new core must be installed in the GUI environment and in the module's environment;
75
+ update standalone bundles as well as normal Python installations. Old core decoders ignore
76
+ unknown `choices`, and old `options: list[str]` decoding cannot recover numerical types.
77
+ New GUI + old producer is therefore only legacy-compatible, not a complete fix. Old
78
+ GUI + new producer continues to render string options and retains its old bug.
79
+
80
+ Readable labels additionally require the model provider to publish the shared
81
+ `x-pyobs-choices` metadata and the module to republish capabilities after restart.
82
+ Test typed choices without labels too, so the numeric fix can ship independently of
83
+ label deployment. Implementation and automated regression checks are complete; release and deployment remain pending.
84
+
85
+ ## Implementation verification (2026-10-15)
86
+
87
+ - Full GUI suite: 311 passed. Structured-config regressions pass with both the
88
+ installed legacy core and the candidate typed-choice core.
89
+ - Ruff and Pyrefly pass; changed Python files pass Black. Repository-wide Black
90
+ reports existing differences in generated UI files and unrelated widgets.
91
+ - Candidate provider → core XML → Qt checks preserve numeric values and labels,
92
+ including unlabeled bitmask combinations. No hardware operation was performed.
@@ -74,6 +74,9 @@ ADRs that concern `pyobs-gui` live in `pyobs-core`'s `specs/` tree instead (`spe
74
74
  single compiled binary that works across sites with no rebuild. **rejected 2026-09-14** — the
75
75
  real build came out several GB, not viable as a one-file download. The two pieces below shipped
76
76
  independently and aren't affected.
77
+ - `pyobs-core/specs/design/gui-install-size-and-standalone-build.md` — slimmed install (1506 to
78
+ 716 MB) and the experimental Nuitka binary (764 MB) with its open astropy unit-format issue.
79
+ **dependency split implemented** (`pyobs-gui` 2.6.1, `pyobs-core` 2.14.0), binary experimental.
77
80
  - `pyobs-core/specs/plans/2026-07-26-gui-interactive-login.md` — interactive login/settings
78
81
  dialog to replace the current YAML-config-file requirement (pyobs-core side).
79
82
  **implemented, closed** (landed 2026-07-27)
@@ -107,3 +110,6 @@ ADRs that concern `pyobs-gui` live in `pyobs-core`'s `specs/` tree instead (`spe
107
110
  live view in `VideoWidget`: MJPEG with server-side stretch for slow links, or `/video.raw` with
108
111
  client-side stretch/cuts and crop on zoom. Plan: `pyobs-core/specs/plans/2026-09-29-basevideo-frame-buffer-redesign.md`
109
112
  (phase 6).
113
+
114
+ - [2026-10-14-typed-config-choices.md](2026-10-14-typed-config-choices.md) — **implemented; release pending** (#211). Typed structured-config combo values and
115
+ readable labels; coordinated core capability and provider metadata dependencies.
@@ -1,3 +1,4 @@
1
+ from types import SimpleNamespace
1
2
  from unittest.mock import AsyncMock, patch
2
3
 
3
4
  import pytest
@@ -376,3 +377,119 @@ async def test_apply_failure_surfaces_through_show_remote_error(qapp) -> None:
376
377
 
377
378
  mock_show_error.assert_awaited_once()
378
379
  widget.close()
380
+
381
+
382
+ @pytest.mark.parametrize("value", [16416, 1.5, True, False, "001"])
383
+ def test_typed_choice_preserves_value_and_label(value) -> None:
384
+ schema = _typed_schema(type="enum", choices=[SimpleNamespace(value=value, label="Detector α")], default=value)
385
+ editor = _build_editor("detector", schema, lambda: None)
386
+ assert isinstance(editor.widget, QtWidgets.QComboBox)
387
+ assert editor.widget.itemText(0) == "Detector α"
388
+ assert editor.get_value() == value
389
+ assert type(editor.get_value()) is type(value)
390
+ editor.set_value(value)
391
+ assert type(editor.get_value()) is type(value)
392
+
393
+
394
+ def test_typed_choice_identity_duplicate_labels_and_unknown_values() -> None:
395
+ values = [1, "1", True, "001"]
396
+ changes = []
397
+ editor = _build_editor(
398
+ "choice",
399
+ _typed_schema(
400
+ type="enum",
401
+ choices=[SimpleNamespace(value=value, label="same") for value in values],
402
+ default=1,
403
+ ),
404
+ lambda: changes.append(True),
405
+ )
406
+ combo = editor.widget
407
+ assert isinstance(combo, QtWidgets.QComboBox)
408
+ for index, value in enumerate(values):
409
+ editor.set_value(value)
410
+ assert combo.currentIndex() == index
411
+ assert type(editor.get_value()) is type(value)
412
+ assert changes # index changes fire even when every display label is the same
413
+ for unknown in [999, None, "missing"]:
414
+ editor.set_value(unknown) # pyrefly: ignore [bad-argument-type] -- intentionally invalid None
415
+ assert editor.get_value() == unknown
416
+ assert combo.count() == len(values) + 1
417
+ assert "unknown" in combo.currentText()
418
+ editor.set_value(1)
419
+ assert combo.count() == len(values)
420
+ combo.setCurrentIndex(-1)
421
+ assert editor.get_value() == 1
422
+
423
+
424
+ def test_empty_choice_domain_preserves_received_value() -> None:
425
+ editor = _build_editor("choice", _typed_schema(type="enum", options=["legacy"], choices=[]), lambda: None)
426
+ editor.set_value(999)
427
+ assert editor.get_value() == 999
428
+ assert isinstance(editor.widget, QtWidgets.QComboBox)
429
+ assert "legacy" not in editor.widget.currentText()
430
+
431
+
432
+ @pytest.mark.asyncio
433
+ async def test_typed_choice_state_reset_and_apply() -> None:
434
+ widget = StructuredConfigWidget()
435
+ widget.modules = ["module"]
436
+ widget._build_form(
437
+ ConfigSchema(
438
+ fields={
439
+ "main": _typed_schema(
440
+ type="object",
441
+ nested={
442
+ "detector": _typed_schema(
443
+ type="enum",
444
+ choices=[SimpleNamespace(value=1, label="A"), SimpleNamespace(value=2, label="B")],
445
+ )
446
+ },
447
+ )
448
+ }
449
+ )
450
+ )
451
+ widget._on_state(ConfigAppliedState(config={"main": {"detector": 1}}))
452
+ widget.update_gui()
453
+ assert not widget._is_dirty()
454
+ assert widget._root is not None and widget._root.children is not None
455
+ main = widget._root.children["main"]
456
+ assert main.children is not None
457
+ editor = main.children["detector"]
458
+ editor.set_value(2)
459
+ assert widget._is_dirty()
460
+ widget.reset_clicked()
461
+ assert editor.get_value() == 1
462
+ assert not widget._is_dirty()
463
+ editor.set_value(2)
464
+ proxy = AsyncMock()
465
+ widget._comm = FakeComm(proxy) # pyrefly: ignore [bad-assignment]
466
+ await widget._apply_config(_nested_get(widget._root.children))
467
+ proxy.set_config.assert_awaited_once_with({"main": {"detector": 2}})
468
+ assert type(proxy.set_config.call_args.args[0]["main"]["detector"]) is int
469
+
470
+
471
+ def _typed_schema(*, choices=None, **kwargs):
472
+ # Build the additive capability shape without requiring an unreleased core in CI.
473
+ schema = ConfigFieldSchema(**kwargs)
474
+ vars(schema)["choices"] = choices
475
+ return schema
476
+
477
+
478
+ def test_numeric_and_boolean_choices_have_distinct_dirty_state() -> None:
479
+ widget = StructuredConfigWidget()
480
+ widget.modules = ["module"]
481
+ widget._build_form(
482
+ ConfigSchema(
483
+ fields={
484
+ "choice": _typed_schema(
485
+ type="enum",
486
+ choices=[SimpleNamespace(value=1, label="Number"), SimpleNamespace(value=True, label="Boolean")],
487
+ )
488
+ }
489
+ )
490
+ )
491
+ widget._on_state(ConfigAppliedState(config={"choice": 1}))
492
+ widget.update_gui()
493
+ assert widget._root is not None and widget._root.children is not None
494
+ widget._root.children["choice"].set_value(True)
495
+ assert widget._is_dirty()