pyobs-gui 2.3.0__tar.gz → 2.3.1__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 (168) hide show
  1. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/PKG-INFO +2 -2
  2. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/structuredconfigwidget.py +63 -6
  3. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyproject.toml +2 -2
  4. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +1 -2
  5. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/specs/2026-09-01-gui-video-widget-split.md +1 -1
  6. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/specs/index.md +16 -12
  7. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_structuredconfigwidget.py +77 -1
  8. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/uv.lock +5 -5
  9. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.claude/skills/verify/SKILL.md +0 -0
  10. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.github/dependabot.yml +0 -0
  11. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.github/workflows/dependabot-automerge.yml +0 -0
  12. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.github/workflows/pypi.yml +0 -0
  13. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.github/workflows/pyrefly.yml +0 -0
  14. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.github/workflows/ruff.yml +0 -0
  15. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.gitignore +0 -0
  16. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.pre-commit-config.yaml +0 -0
  17. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/.readthedocs.yml +0 -0
  18. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/CHANGELOG.rst +0 -0
  19. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/LICENSE +0 -0
  20. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/README.md +0 -0
  21. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/bin/mypyuic5 +0 -0
  22. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/docs/Makefile +0 -0
  23. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/docs/make.bat +0 -0
  24. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/docs/requirements.txt +0 -0
  25. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/docs/source/_static/pyobs.gif +0 -0
  26. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/docs/source/conf.py +0 -0
  27. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/docs/source/index.rst +0 -0
  28. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/mypy.ini +0 -0
  29. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/__init__.py +0 -0
  30. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/__main__.py +0 -0
  31. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
  32. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/accounts.py +0 -0
  33. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/acquisitionwidget.py +0 -0
  34. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/autofocuswidget.py +0 -0
  35. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/autoguidingwidget.py +0 -0
  36. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/base.py +0 -0
  37. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/camerawidget.py +0 -0
  38. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/commandinputwidget.py +0 -0
  39. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/compassmovewidget.py +0 -0
  40. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/coolingwidget.py +0 -0
  41. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/datadisplaywidget.py +0 -0
  42. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/eventswidget.py +0 -0
  43. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/filterwidget.py +0 -0
  44. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/fitsheaderswidget.py +0 -0
  45. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/focuswidget.py +0 -0
  46. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/gui.py +0 -0
  47. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/login.py +0 -0
  48. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/loginwindow.py +0 -0
  49. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/logmodel.py +0 -0
  50. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/mainwindow.py +0 -0
  51. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/modewidget.py +0 -0
  52. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/modifiedmixin.py +0 -0
  53. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/modulegui.py +0 -0
  54. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/__init__.py +0 -0
  55. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  56. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  57. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  58. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  59. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  60. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  61. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/camerawidget.ui +0 -0
  62. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  63. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  64. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/compassmovewidget.py +0 -0
  65. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  66. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  67. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/compile.sh +0 -0
  68. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/coolingwidget.ui +0 -0
  69. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  70. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  71. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  72. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  73. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/designer.sh +0 -0
  74. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/eventswidget.ui +0 -0
  75. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  76. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/filterwidget.ui +0 -0
  77. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  78. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  79. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  80. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/focuswidget.ui +0 -0
  81. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  82. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/mainwindow.ui +0 -0
  83. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  84. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/modewidget.ui +0 -0
  85. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/modewidget_ui.py +0 -0
  86. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  87. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  88. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  89. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  90. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  91. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  92. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  93. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  94. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  95. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  96. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  97. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  98. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources.qrc +0 -0
  99. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/resources_rc.py +0 -0
  100. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/roboticwidget.ui +0 -0
  101. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
  102. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/roofwidget.ui +0 -0
  103. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  104. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/schedulewidget.ui +0 -0
  105. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
  106. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/shellwidget.ui +0 -0
  107. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  108. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  109. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  110. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/telescopewidget.ui +0 -0
  111. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  112. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  113. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  114. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  115. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  116. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/videograbwidget.ui +0 -0
  117. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/videograbwidget_ui.py +0 -0
  118. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/videowidget.ui +0 -0
  119. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/videowidget_ui.py +0 -0
  120. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/weatherwidget.ui +0 -0
  121. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  122. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/roboticwidget.py +0 -0
  123. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/roofwidget.py +0 -0
  124. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/schedulewidget.py +0 -0
  125. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/shellwidget.py +0 -0
  126. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/spectrographwidget.py +0 -0
  127. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/statuswidget.py +0 -0
  128. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/telescopewidget.py +0 -0
  129. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/temperaturesplotwidget.py +0 -0
  130. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/temperatureswidget.py +0 -0
  131. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/utils.py +0 -0
  132. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/videograbwidget.py +0 -0
  133. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/videowidget.py +0 -0
  134. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/watchedlabel.py +0 -0
  135. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pyobs_gui/weatherwidget.py +0 -0
  136. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/pysidedeploy.spec +0 -0
  137. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  138. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
  139. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/specs/2026-08-28-structuredconfig-widget.md +0 -0
  140. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/specs/2026-08-31-irobotic-widgets.md +0 -0
  141. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/spike_standalone/main.py +0 -0
  142. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/spike_standalone/pysidedeploy.spec +0 -0
  143. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/acquisition.yaml +0 -0
  144. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/autofocus.yaml +0 -0
  145. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/camera.yaml +0 -0
  146. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/full.yaml +0 -0
  147. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/guiding.yaml +0 -0
  148. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/mode.yaml +0 -0
  149. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/robotic.yaml +0 -0
  150. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/roof.yaml +0 -0
  151. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/spectrograph.yaml +0 -0
  152. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/structuredconfig.yaml +0 -0
  153. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/telescope.yaml +0 -0
  154. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/telescope_acl.yaml +0 -0
  155. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/telescope_acl_denied.yaml +0 -0
  156. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/test/video.yaml +0 -0
  157. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/__init__.py +0 -0
  158. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/conftest.py +0 -0
  159. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_accounts.py +0 -0
  160. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_camerawidget.py +0 -0
  161. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_loginwindow.py +0 -0
  162. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_mainwindow_startup.py +0 -0
  163. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_modewidget.py +0 -0
  164. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_multiwidget_pages.py +0 -0
  165. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_roboticwidget.py +0 -0
  166. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_schedulewidget.py +0 -0
  167. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_videograbwidget.py +0 -0
  168. {pyobs_gui-2.3.0 → pyobs_gui-2.3.1}/tests/test_videowidget.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-gui
3
- Version: 2.3.0
3
+ Version: 2.3.1
4
4
  Summary: A remote GUI for pyobs
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -11,7 +11,7 @@ Requires-Dist: astropy<8,>=7.0.1
11
11
  Requires-Dist: colour<0.2,>=0.1.5
12
12
  Requires-Dist: keyring<26,>=25.7.0
13
13
  Requires-Dist: matplotlib<4,>=3.10.1
14
- Requires-Dist: pyobs-core<3,>=2.2.0
14
+ Requires-Dist: pyobs-core<3,>=2.7.1
15
15
  Requires-Dist: pyside6>=6.10.1
16
16
  Requires-Dist: qasync<0.29,>=0.27.1
17
17
  Requires-Dist: qfitswidget>=0.13.1
@@ -1,11 +1,12 @@
1
1
  import dataclasses
2
2
  import logging
3
- from typing import Any, Callable, Dict, Optional, cast
3
+ from typing import Any, Callable, Dict, List, Optional, cast
4
4
 
5
5
  from PySide6 import QtCore, QtWidgets # type: ignore
6
6
 
7
7
  from pyobs.interfaces import ConfigAppliedState, ConfigFieldSchema, ConfigSchema, IStructuredConfig
8
8
  from pyobs.interfaces.IStructuredConfig import ConfigValue
9
+ from pyobs.utils.enums import AccessLevel
9
10
  from .base import BaseWidget
10
11
 
11
12
  log = logging.getLogger(__name__)
@@ -27,6 +28,16 @@ class _FieldEditor:
27
28
  get_value: Callable[[], ConfigValue]
28
29
  set_value: Callable[[ConfigValue], None]
29
30
  children: Optional[Dict[str, "_FieldEditor"]] = None
31
+ level: AccessLevel = AccessLevel.BASIC
32
+
33
+
34
+ def _collect_by_level(editor: "_FieldEditor", level: AccessLevel) -> List[QtWidgets.QWidget]:
35
+ """Recursively gather every editor widget (including nested-object children) tagged at
36
+ exactly the given level, for the basic/expert toggle to enable/disable as a group."""
37
+ widgets = [editor.widget] if editor.level == level else []
38
+ for child in (editor.children or {}).values():
39
+ widgets.extend(_collect_by_level(child, level))
40
+ return widgets
30
41
 
31
42
 
32
43
  def _nested_get(children: Dict[str, _FieldEditor]) -> Dict[str, ConfigValue]:
@@ -49,7 +60,9 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
49
60
  if schema.default is not None:
50
61
  line_edit.setText(str(schema.default))
51
62
  line_edit.textChanged.connect(lambda _: on_change())
52
- return _FieldEditor(line_edit, line_edit.text, lambda value: line_edit.setText(str(value)))
63
+ if schema.level == AccessLevel.EXPERT:
64
+ line_edit.setEnabled(False)
65
+ return _FieldEditor(line_edit, line_edit.text, lambda value: line_edit.setText(str(value)), level=schema.level)
53
66
 
54
67
  if schema.type == "int":
55
68
  spin_box = QtWidgets.QSpinBox()
@@ -57,7 +70,11 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
57
70
  if schema.default is not None:
58
71
  spin_box.setValue(int(schema.default))
59
72
  spin_box.valueChanged.connect(lambda _: on_change())
60
- return _FieldEditor(spin_box, spin_box.value, lambda value: spin_box.setValue(int(cast("int", value))))
73
+ if schema.level == AccessLevel.EXPERT:
74
+ spin_box.setEnabled(False)
75
+ return _FieldEditor(
76
+ spin_box, spin_box.value, lambda value: spin_box.setValue(int(cast("int", value))), level=schema.level
77
+ )
61
78
 
62
79
  if schema.type == "float":
63
80
  double_spin_box = QtWidgets.QDoubleSpinBox()
@@ -68,8 +85,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
68
85
  if schema.default is not None:
69
86
  double_spin_box.setValue(float(schema.default))
70
87
  double_spin_box.valueChanged.connect(lambda _: on_change())
88
+ if schema.level == AccessLevel.EXPERT:
89
+ double_spin_box.setEnabled(False)
71
90
  return _FieldEditor(
72
- double_spin_box, double_spin_box.value, lambda value: double_spin_box.setValue(float(cast("float", value)))
91
+ double_spin_box,
92
+ double_spin_box.value,
93
+ lambda value: double_spin_box.setValue(float(cast("float", value))),
94
+ level=schema.level,
73
95
  )
74
96
 
75
97
  if schema.type == "bool":
@@ -77,7 +99,11 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
77
99
  if schema.default is not None:
78
100
  check_box.setChecked(bool(schema.default))
79
101
  check_box.toggled.connect(lambda _: on_change())
80
- return _FieldEditor(check_box, check_box.isChecked, lambda value: check_box.setChecked(bool(value)))
102
+ if schema.level == AccessLevel.EXPERT:
103
+ check_box.setEnabled(False)
104
+ return _FieldEditor(
105
+ check_box, check_box.isChecked, lambda value: check_box.setChecked(bool(value)), level=schema.level
106
+ )
81
107
 
82
108
  if schema.type == "enum":
83
109
  combo_box = QtWidgets.QComboBox()
@@ -86,7 +112,14 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
86
112
  if schema.default is not None and str(schema.default) in options:
87
113
  combo_box.setCurrentText(str(schema.default))
88
114
  combo_box.currentTextChanged.connect(lambda _: on_change())
89
- return _FieldEditor(combo_box, combo_box.currentText, lambda value: combo_box.setCurrentText(str(value)))
115
+ if schema.level == AccessLevel.EXPERT:
116
+ combo_box.setEnabled(False)
117
+ return _FieldEditor(
118
+ combo_box,
119
+ combo_box.currentText,
120
+ lambda value: combo_box.setCurrentText(str(value)),
121
+ level=schema.level,
122
+ )
90
123
 
91
124
  if schema.type == "object" and schema.nested is not None:
92
125
  group_box = QtWidgets.QGroupBox(name)
@@ -94,14 +127,19 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
94
127
  group_box.setLayout(form_layout)
95
128
  children: Dict[str, _FieldEditor] = {}
96
129
  for child_name, child_schema in schema.nested.items():
130
+ if child_schema.level == AccessLevel.HIDDEN:
131
+ continue
97
132
  child_editor = _build_editor(child_name, child_schema, on_change)
98
133
  form_layout.addRow(child_name, child_editor.widget)
99
134
  children[child_name] = child_editor
135
+ if schema.level == AccessLevel.EXPERT:
136
+ group_box.setEnabled(False)
100
137
  return _FieldEditor(
101
138
  group_box,
102
139
  lambda: _nested_get(children),
103
140
  lambda value: _nested_set(children, value),
104
141
  children=children,
142
+ level=schema.level,
105
143
  )
106
144
 
107
145
  # object without a nested schema (pydantic freeform dict), or any other schema type this
@@ -120,6 +158,8 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
120
158
  placeholder.setReadOnly(True)
121
159
  placeholder.setEnabled(False)
122
160
  placeholder.setToolTip(f"Field type {schema.type!r} has no schema-driven editor for this widget.")
161
+ # always BASIC regardless of schema.level: this widget is permanently disabled (nothing to
162
+ # edit), so it must never be swept into the expert-toggle's re-enable group
123
163
  return _FieldEditor(placeholder, _get_raw, _set_raw)
124
164
 
125
165
 
@@ -142,10 +182,19 @@ class StructuredConfigWidget(BaseWidget):
142
182
  # itself register as a user edit (which would otherwise re-enable Apply/Reset for a
143
183
  # change that came from the module, not from the operator)
144
184
  self._applying_state = False
185
+ # every EXPERT-level editor widget across the whole (possibly nested) form, collected
186
+ # once in _build_form; toggled together by checkBoxAdvanced. No password gate, unlike
187
+ # pyftscontrol's Expert Mode button -- operators here are already authenticated via the
188
+ # comm layer, so it's just a basic/expert visibility toggle, not an access control.
189
+ self._expert_widgets: List[QtWidgets.QWidget] = []
145
190
 
146
191
  outer_layout = QtWidgets.QVBoxLayout()
147
192
  self.setLayout(outer_layout)
148
193
 
194
+ self.checkBoxAdvanced = QtWidgets.QCheckBox("Show advanced fields")
195
+ self.checkBoxAdvanced.toggled.connect(self._on_advanced_toggled)
196
+ outer_layout.addWidget(self.checkBoxAdvanced)
197
+
149
198
  scroll_area = QtWidgets.QScrollArea()
150
199
  scroll_area.setWidgetResizable(True)
151
200
  self._form_container = QtWidgets.QWidget()
@@ -186,6 +235,8 @@ class StructuredConfigWidget(BaseWidget):
186
235
  def _build_form(self, schema: ConfigSchema) -> None:
187
236
  children: Dict[str, _FieldEditor] = {}
188
237
  for name, field_schema in schema.fields.items():
238
+ if field_schema.level == AccessLevel.HIDDEN:
239
+ continue
189
240
  editor = _build_editor(name, field_schema, self._on_editor_changed)
190
241
  self._form_layout.addRow(name, editor.widget)
191
242
  children[name] = editor
@@ -195,11 +246,17 @@ class StructuredConfigWidget(BaseWidget):
195
246
  lambda value: _nested_set(children, value),
196
247
  children=children,
197
248
  )
249
+ self._expert_widgets = _collect_by_level(self._root, AccessLevel.EXPERT)
250
+ self._on_advanced_toggled(self.checkBoxAdvanced.isChecked())
198
251
  # a state may already have arrived (and been cached) before the form existed to receive
199
252
  # it -- apply it now instead of waiting for the next update
200
253
  if self._last_applied is not None:
201
254
  self.signal_update_gui.emit()
202
255
 
256
+ def _on_advanced_toggled(self, checked: bool) -> None:
257
+ for widget in self._expert_widgets:
258
+ widget.setEnabled(checked)
259
+
203
260
  def _on_state(self, state: ConfigAppliedState) -> None:
204
261
  self._last_applied = dict(state.config)
205
262
  self.signal_update_gui.emit()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-gui"
3
- version = "2.3.0"
3
+ version = "2.3.1"
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"
@@ -12,7 +12,7 @@ dependencies = [
12
12
  "sunpy[all]>=7.0.1",
13
13
  "QtAwesome>=1.4.0,<2",
14
14
  "qfitswidget>=0.13.1",
15
- "pyobs-core>=2.2.0,<3",
15
+ "pyobs-core>=2.7.1,<3",
16
16
  "pyside6>=6.10.1",
17
17
  "astroplan>=0.10.1",
18
18
  "astropy>=7.0.1,<8",
@@ -1,7 +1,6 @@
1
1
  # Plan: pyobs-gui — main widgets vs. sidebar widgets, automatic tab pages for multi-widget modules
2
2
 
3
- Status: implemented, merged to develop (PR #157, `b7a14a6`) — issue #150 stays open until this
4
- reaches a release on `main`, per repo convention.
3
+ Status: implemented, closed (PR #157, `b7a14a6`; released `v2.3.0`, `main`). Issue #150 closed.
5
4
  Audited: 2026-08-28
6
5
  Revised: 2026-09-01 (D1, D2 — see "Revision 2026-09-01" below; adds D6)
7
6
 
@@ -1,6 +1,6 @@
1
1
  # `VideoWidget` split into two independent main widgets
2
2
 
3
- Status: implemented, closed.
3
+ Status: implemented, closed (`cf42ab4`; released `v2.3.0`).
4
4
 
5
5
  Dependency landed 2026-09-01: `2026-08-28-gui-main-vs-sidebar-widgets.md` merged to develop as
6
6
  PR #157 (`b7a14a6`).
@@ -13,18 +13,22 @@ ADRs that concern `pyobs-gui` live in `pyobs-core`'s `specs/` tree instead (`spe
13
13
  - `2026-08-28-structuredconfig-widget.md` — **proposed (issue #154)**. Generic
14
14
  `StructuredConfigWidget`: schema-driven, auto-built editable form for `IStructuredConfig`
15
15
  modules (from `ConfigSchema` capabilities + `ConfigAppliedState` + `set_config`).
16
- - `2026-08-28-gui-main-vs-sidebar-widgets.md` — **implemented, merged to develop (PR #157,
17
- `b7a14a6`)**. Issue #150 stays open until this reaches a release on `main`. Explicit
18
- main-widget/sidebar-widget distinction, automatic tab pages for multi-widget modules (shared
19
- sidebar, merge-per-interface custom config, standalone parity), `sidebar_preferred` promotion
20
- rule, universal sidebar container, `paired_sidebar_widget` mechanism (D6, no consumer yet).
21
- Post-merge follow-ups: sidebar-fill dedup + D5 sidebar-failure isolation + 3 other review
22
- findings (`fccf0e0`), FITS-headers sidebar box alignment (`1e89069`), vertical-only scrollable
23
- sidebar (`f05bcba`).
24
- - `2026-09-01-gui-video-widget-split.md` — **implemented, closed**. Split `VideoWidget` into two
25
- independent main widgets (`VideoWidget` "Live View" + new `VideoGrabWidget` "FITS Image"), both
26
- matched on `IVideo`, rendered as separate tabs — turned out not to need the `paired_sidebar_widget`
27
- mechanism (D6) the draft was written for; the two halves never shared state to begin with.
16
+ - `2026-08-28-gui-main-vs-sidebar-widgets.md` — **implemented, closed (PR #157, `b7a14a6`;
17
+ released `v2.3.0`)**. Issue #150 closed. Explicit main-widget/sidebar-widget distinction,
18
+ automatic tab pages for multi-widget modules (shared sidebar, merge-per-interface custom
19
+ config, standalone parity), `sidebar_preferred` promotion rule, universal sidebar container,
20
+ `paired_sidebar_widget` mechanism (D6, still no consumer — the video-widget split below ended up
21
+ not needing it). Post-merge follow-ups: sidebar-fill dedup + D5 sidebar-failure isolation + 3
22
+ other review findings (`fccf0e0`), FITS-headers sidebar box alignment (`1e89069`),
23
+ vertical-only scrollable sidebar (`f05bcba`).
24
+ - `2026-09-01-gui-video-widget-split.md` — **implemented, closed, released `v2.3.0`**. Split
25
+ `VideoWidget` into two independent main widgets (`VideoWidget` "Live View" + new
26
+ `VideoGrabWidget` "FITS Image"), both matched on `IVideo`, rendered as separate tabs — turned out
27
+ not to need the `paired_sidebar_widget` mechanism (D6) the draft was written for; the two halves
28
+ never shared state to begin with. Same release also carries two unplanned same-day fixes with no
29
+ dedicated doc: `CameraWidget`'s control panel clipping its right edge (`scrollArea` sized off a
30
+ stale `.ui` width hint instead of real content, `c22d7c7`) and `ModeWidget`'s edit button staying
31
+ permanently disabled for an `IMode` module with no `IMotion` (`1439437`).
28
32
  - `2026-08-21-gui-widget-startup-responsiveness.md` — **implemented, closed 2026-08-23 (PR #141,
29
33
  `123161b`)**. Make module widgets appear and respond immediately at startup: non-blocking
30
34
  `_add_client` with a "Loading…" placeholder page (clicks are never dropped), parallel
@@ -5,7 +5,7 @@ from PySide6 import QtWidgets # type: ignore
5
5
 
6
6
  from pyobs.interfaces import ConfigAppliedState, ConfigFieldSchema, ConfigSchema
7
7
  from pyobs.utils import exceptions as exc
8
- from pyobs.utils.enums import Unit
8
+ from pyobs.utils.enums import AccessLevel, Unit
9
9
  from pyobs_gui.structuredconfigwidget import StructuredConfigWidget, _build_editor, _nested_get
10
10
 
11
11
  # -- shared fixtures -----------------------------------------------------------------------
@@ -152,6 +152,82 @@ def test_unsupported_field_type_is_also_a_disabled_placeholder() -> None:
152
152
  assert editor.get_value() == [1, 2]
153
153
 
154
154
 
155
+ # -- _build_editor / _build_form: basic/expert level ----------------------------------------
156
+
157
+
158
+ def test_basic_field_starts_enabled() -> None:
159
+ editor = _build_editor("count", ConfigFieldSchema(type="int", default=3, level=AccessLevel.BASIC), lambda: None)
160
+ assert editor.widget.isEnabled() is True
161
+ assert editor.level == AccessLevel.BASIC
162
+
163
+
164
+ def test_expert_field_starts_disabled() -> None:
165
+ editor = _build_editor("count", ConfigFieldSchema(type="int", default=3, level=AccessLevel.EXPERT), lambda: None)
166
+ assert editor.widget.isEnabled() is False
167
+ assert editor.level == AccessLevel.EXPERT
168
+
169
+
170
+ def test_unsupported_type_placeholder_ignores_expert_level_for_toggle_grouping() -> None:
171
+ """A permanently non-editable placeholder must never be swept into the expert-toggle's
172
+ re-enable group, even if the underlying field is tagged EXPERT -- see the comment in
173
+ _build_editor's fallback branch."""
174
+ editor = _build_editor(
175
+ "items", ConfigFieldSchema(type="list", default=[1, 2], level=AccessLevel.EXPERT), lambda: None
176
+ )
177
+ assert editor.level == AccessLevel.BASIC
178
+ assert editor.widget.isEnabled() is False
179
+
180
+
181
+ def test_nested_object_skips_hidden_children_and_disables_whole_group_when_expert() -> None:
182
+ schema = ConfigFieldSchema(
183
+ type="object",
184
+ level=AccessLevel.EXPERT,
185
+ nested={
186
+ "az": ConfigFieldSchema(type="float", default=1.0),
187
+ "secret": ConfigFieldSchema(type="float", default=2.0, level=AccessLevel.HIDDEN),
188
+ },
189
+ )
190
+ editor = _build_editor("pointing", schema, lambda: None)
191
+ assert editor.children is not None and set(editor.children) == {"az"}
192
+ assert editor.widget.isEnabled() is False
193
+
194
+
195
+ def _sample_schema_with_levels() -> ConfigSchema:
196
+ return ConfigSchema(
197
+ fields={
198
+ "name": ConfigFieldSchema(type="str", default="foo", level=AccessLevel.BASIC),
199
+ "gain": ConfigFieldSchema(type="int", default=1, level=AccessLevel.EXPERT),
200
+ "secret": ConfigFieldSchema(type="int", default=0, level=AccessLevel.HIDDEN),
201
+ }
202
+ )
203
+
204
+
205
+ def test_build_form_skips_hidden_fields_entirely() -> None:
206
+ widget = StructuredConfigWidget()
207
+ widget._build_form(_sample_schema_with_levels())
208
+
209
+ assert widget._form_layout.rowCount() == 2 # name + gain, not secret
210
+ assert widget._root is not None and widget._root.children is not None
211
+ assert set(widget._root.children) == {"name", "gain"}
212
+ widget.close()
213
+
214
+
215
+ def test_advanced_checkbox_toggles_expert_fields_enabled() -> None:
216
+ widget = StructuredConfigWidget()
217
+ widget._build_form(_sample_schema_with_levels())
218
+ assert widget._root is not None and widget._root.children is not None
219
+ expert_widget = widget._root.children["gain"].widget
220
+
221
+ assert expert_widget.isEnabled() is False # starts locked
222
+
223
+ widget.checkBoxAdvanced.setChecked(True)
224
+ assert expert_widget.isEnabled() is True
225
+
226
+ widget.checkBoxAdvanced.setChecked(False)
227
+ assert expert_widget.isEnabled() is False
228
+ widget.close()
229
+
230
+
155
231
  # -- StructuredConfigWidget: state population, dirty tracking, apply/reset ------------------
156
232
 
157
233
 
@@ -3256,7 +3256,7 @@ wheels = [
3256
3256
 
3257
3257
  [[package]]
3258
3258
  name = "pyobs-core"
3259
- version = "2.2.0"
3259
+ version = "2.7.1"
3260
3260
  source = { registry = "https://pypi.org/simple" }
3261
3261
  dependencies = [
3262
3262
  { name = "aiohttp" },
@@ -3288,14 +3288,14 @@ dependencies = [
3288
3288
  { name = "tenacity" },
3289
3289
  { name = "typing-extensions" },
3290
3290
  ]
3291
- sdist = { url = "https://files.pythonhosted.org/packages/7b/77/f145aa80dcc2a3813576609f9a92bd565a9db9d61777f9cc124afb1878cc/pyobs_core-2.2.0.tar.gz", hash = "sha256:adede5c3c12722565e5ccb32f5fe49397dc14e74674c6778482d40f81ca300e1", size = 395720, upload-time = "2026-09-01T11:47:40.797Z" }
3291
+ sdist = { url = "https://files.pythonhosted.org/packages/9d/5e/8d4de011685819cd4b04ee8a5aa35822814e0d6c607f4dfe6917b37e71e2/pyobs_core-2.7.1.tar.gz", hash = "sha256:beb50633cc21b6ea5a99713fe114973eecf7999a17f69578a996f4335fcb1ed7", size = 409990, upload-time = "2026-09-04T09:44:17.492Z" }
3292
3292
  wheels = [
3293
- { url = "https://files.pythonhosted.org/packages/9c/84/5cf38ef1f504b0c7e76e38a84d7f721b9b4cdb68f389f1263591c75324fc/pyobs_core-2.2.0-py3-none-any.whl", hash = "sha256:9b14d86efff1360ee00d4cfcbf4a89ab6f8089f9c60de0d7e404a239da7a524d", size = 624091, upload-time = "2026-09-01T11:47:39.082Z" },
3293
+ { url = "https://files.pythonhosted.org/packages/fd/14/037a4e52b30816474fb4360a0741952499820e24af01c09f609311369379/pyobs_core-2.7.1-py3-none-any.whl", hash = "sha256:060de003b319e4f7a14dd80500d13d1d202df1e49364af74dac3e7715ff843cb", size = 639800, upload-time = "2026-09-04T09:44:16.291Z" },
3294
3294
  ]
3295
3295
 
3296
3296
  [[package]]
3297
3297
  name = "pyobs-gui"
3298
- version = "2.3.0"
3298
+ version = "2.3.1"
3299
3299
  source = { editable = "." }
3300
3300
  dependencies = [
3301
3301
  { name = "astroplan" },
@@ -3339,7 +3339,7 @@ requires-dist = [
3339
3339
  { name = "colour", specifier = ">=0.1.5,<0.2" },
3340
3340
  { name = "keyring", specifier = ">=25.7.0,<26" },
3341
3341
  { name = "matplotlib", specifier = ">=3.10.1,<4" },
3342
- { name = "pyobs-core", specifier = ">=2.2.0,<3" },
3342
+ { name = "pyobs-core", specifier = ">=2.7.1,<3" },
3343
3343
  { name = "pyside6", specifier = ">=6.10.1" },
3344
3344
  { name = "qasync", specifier = ">=0.27.1,<0.29" },
3345
3345
  { name = "qfitswidget", specifier = ">=0.13.1" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes