pyobs-gui 2.3.0__tar.gz → 2.4.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 (168) hide show
  1. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/PKG-INFO +2 -2
  2. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/structuredconfigwidget.py +113 -9
  3. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyproject.toml +2 -2
  4. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +1 -2
  5. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-09-01-gui-video-widget-split.md +1 -1
  6. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/index.md +16 -12
  7. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_structuredconfigwidget.py +126 -1
  8. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/uv.lock +5 -5
  9. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.claude/skills/verify/SKILL.md +0 -0
  10. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/dependabot.yml +0 -0
  11. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/workflows/dependabot-automerge.yml +0 -0
  12. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/workflows/pypi.yml +0 -0
  13. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/workflows/pyrefly.yml +0 -0
  14. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.github/workflows/ruff.yml +0 -0
  15. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.gitignore +0 -0
  16. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.pre-commit-config.yaml +0 -0
  17. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/.readthedocs.yml +0 -0
  18. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/CHANGELOG.rst +0 -0
  19. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/LICENSE +0 -0
  20. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/README.md +0 -0
  21. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/bin/mypyuic5 +0 -0
  22. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/Makefile +0 -0
  23. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/make.bat +0 -0
  24. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/requirements.txt +0 -0
  25. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/source/_static/pyobs.gif +0 -0
  26. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/source/conf.py +0 -0
  27. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/docs/source/index.rst +0 -0
  28. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/mypy.ini +0 -0
  29. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/__init__.py +0 -0
  30. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/__main__.py +0 -0
  31. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
  32. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/accounts.py +0 -0
  33. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/acquisitionwidget.py +0 -0
  34. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/autofocuswidget.py +0 -0
  35. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/autoguidingwidget.py +0 -0
  36. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/base.py +0 -0
  37. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/camerawidget.py +0 -0
  38. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/commandinputwidget.py +0 -0
  39. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/compassmovewidget.py +0 -0
  40. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/coolingwidget.py +0 -0
  41. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/datadisplaywidget.py +0 -0
  42. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/eventswidget.py +0 -0
  43. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/filterwidget.py +0 -0
  44. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/fitsheaderswidget.py +0 -0
  45. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/focuswidget.py +0 -0
  46. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/gui.py +0 -0
  47. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/login.py +0 -0
  48. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/loginwindow.py +0 -0
  49. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/logmodel.py +0 -0
  50. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/mainwindow.py +0 -0
  51. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/modewidget.py +0 -0
  52. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/modifiedmixin.py +0 -0
  53. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/modulegui.py +0 -0
  54. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/__init__.py +0 -0
  55. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  56. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  57. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  58. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  59. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  60. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  61. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/camerawidget.ui +0 -0
  62. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  63. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  64. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget.py +0 -0
  65. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  66. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  67. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/compile.sh +0 -0
  68. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/coolingwidget.ui +0 -0
  69. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  70. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  71. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  72. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  73. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/designer.sh +0 -0
  74. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/eventswidget.ui +0 -0
  75. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  76. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/filterwidget.ui +0 -0
  77. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  78. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  79. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  80. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/focuswidget.ui +0 -0
  81. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  82. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/mainwindow.ui +0 -0
  83. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  84. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/modewidget.ui +0 -0
  85. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/modewidget_ui.py +0 -0
  86. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  87. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  88. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  89. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  90. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  91. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  92. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  93. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  94. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  95. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  96. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  97. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  98. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources.qrc +0 -0
  99. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/resources_rc.py +0 -0
  100. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/roboticwidget.ui +0 -0
  101. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
  102. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/roofwidget.ui +0 -0
  103. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  104. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/schedulewidget.ui +0 -0
  105. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
  106. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/shellwidget.ui +0 -0
  107. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  108. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  109. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  110. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/telescopewidget.ui +0 -0
  111. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  112. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  113. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  114. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  115. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  116. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/videograbwidget.ui +0 -0
  117. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/videograbwidget_ui.py +0 -0
  118. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/videowidget.ui +0 -0
  119. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/videowidget_ui.py +0 -0
  120. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/weatherwidget.ui +0 -0
  121. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  122. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/roboticwidget.py +0 -0
  123. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/roofwidget.py +0 -0
  124. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/schedulewidget.py +0 -0
  125. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/shellwidget.py +0 -0
  126. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/spectrographwidget.py +0 -0
  127. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/statuswidget.py +0 -0
  128. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/telescopewidget.py +0 -0
  129. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/temperaturesplotwidget.py +0 -0
  130. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/temperatureswidget.py +0 -0
  131. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/utils.py +0 -0
  132. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/videograbwidget.py +0 -0
  133. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/videowidget.py +0 -0
  134. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/watchedlabel.py +0 -0
  135. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pyobs_gui/weatherwidget.py +0 -0
  136. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/pysidedeploy.spec +0 -0
  137. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  138. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
  139. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-28-structuredconfig-widget.md +0 -0
  140. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/specs/2026-08-31-irobotic-widgets.md +0 -0
  141. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/spike_standalone/main.py +0 -0
  142. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/spike_standalone/pysidedeploy.spec +0 -0
  143. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/acquisition.yaml +0 -0
  144. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/autofocus.yaml +0 -0
  145. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/camera.yaml +0 -0
  146. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/full.yaml +0 -0
  147. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/guiding.yaml +0 -0
  148. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/mode.yaml +0 -0
  149. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/robotic.yaml +0 -0
  150. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/roof.yaml +0 -0
  151. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/spectrograph.yaml +0 -0
  152. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/structuredconfig.yaml +0 -0
  153. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/telescope.yaml +0 -0
  154. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/telescope_acl.yaml +0 -0
  155. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/telescope_acl_denied.yaml +0 -0
  156. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/test/video.yaml +0 -0
  157. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/__init__.py +0 -0
  158. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/conftest.py +0 -0
  159. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_accounts.py +0 -0
  160. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_camerawidget.py +0 -0
  161. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_loginwindow.py +0 -0
  162. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_mainwindow_startup.py +0 -0
  163. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_modewidget.py +0 -0
  164. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_multiwidget_pages.py +0 -0
  165. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_roboticwidget.py +0 -0
  166. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_schedulewidget.py +0 -0
  167. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/tests/test_videograbwidget.py +0 -0
  168. {pyobs_gui-2.3.0 → pyobs_gui-2.4.0}/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.4.0
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,49 @@ 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
+ # the row's label, so the basic/expert toggle can hide it alongside `display_widget` --
33
+ # addRow(str, widget) auto-creates one, but we build it explicitly at each row-adding call
34
+ # site instead so we have a handle on it (QFormLayout.labelForField exists but is keyed
35
+ # per-layout-instance, awkward to thread through recursion; a direct reference is simpler)
36
+ row_label: Optional[QtWidgets.QLabel] = None
37
+ # set only when the field has a description: a small container with `widget` on top and the
38
+ # description label below it. `widget` itself (get_value/set_value's target) never changes
39
+ # shape, so a description doesn't affect how values round-trip -- only what goes in the row.
40
+ row_widget: Optional[QtWidgets.QWidget] = None
41
+
42
+ @property
43
+ def display_widget(self) -> QtWidgets.QWidget:
44
+ return self.row_widget if self.row_widget is not None else self.widget
45
+
46
+
47
+ def _collect_by_level(editor: "_FieldEditor", level: AccessLevel) -> List["_FieldEditor"]:
48
+ """Recursively gather every editor (including nested-object children) tagged at exactly the
49
+ given level, for the basic/expert toggle to show/hide as a group."""
50
+ editors = [editor] if editor.level == level else []
51
+ for child in (editor.children or {}).values():
52
+ editors.extend(_collect_by_level(child, level))
53
+ return editors
54
+
55
+
56
+ def _wrap_with_description(widget: QtWidgets.QWidget, description: Optional[str]) -> Optional[QtWidgets.QWidget]:
57
+ """Stack `widget` over a dimmed, word-wrapped description label, for the field's row to show
58
+ the description below the input rather than only as e.g. a tooltip. Returns None (meaning:
59
+ just use `widget` directly, no wrapping) when there's no description."""
60
+ if not description:
61
+ return None
62
+ container = QtWidgets.QWidget()
63
+ layout = QtWidgets.QVBoxLayout(container)
64
+ layout.setContentsMargins(0, 0, 0, 0)
65
+ layout.setSpacing(2)
66
+ layout.addWidget(widget)
67
+ description_label = QtWidgets.QLabel(description)
68
+ description_label.setWordWrap(True)
69
+ # palette(mid) is a 3D-effect shade (borders/shadows), too dark to read comfortably as text;
70
+ # placeholder-text is Qt's actual semantic role for secondary-but-legible text
71
+ description_label.setStyleSheet("color: palette(placeholder-text);")
72
+ layout.addWidget(description_label)
73
+ return container
30
74
 
31
75
 
32
76
  def _nested_get(children: Dict[str, _FieldEditor]) -> Dict[str, ConfigValue]:
@@ -49,7 +93,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
49
93
  if schema.default is not None:
50
94
  line_edit.setText(str(schema.default))
51
95
  line_edit.textChanged.connect(lambda _: on_change())
52
- return _FieldEditor(line_edit, line_edit.text, lambda value: line_edit.setText(str(value)))
96
+ return _FieldEditor(
97
+ line_edit,
98
+ line_edit.text,
99
+ lambda value: line_edit.setText(str(value)),
100
+ level=schema.level,
101
+ row_widget=_wrap_with_description(line_edit, schema.description),
102
+ )
53
103
 
54
104
  if schema.type == "int":
55
105
  spin_box = QtWidgets.QSpinBox()
@@ -57,7 +107,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
57
107
  if schema.default is not None:
58
108
  spin_box.setValue(int(schema.default))
59
109
  spin_box.valueChanged.connect(lambda _: on_change())
60
- return _FieldEditor(spin_box, spin_box.value, lambda value: spin_box.setValue(int(cast("int", value))))
110
+ return _FieldEditor(
111
+ spin_box,
112
+ spin_box.value,
113
+ lambda value: spin_box.setValue(int(cast("int", value))),
114
+ level=schema.level,
115
+ row_widget=_wrap_with_description(spin_box, schema.description),
116
+ )
61
117
 
62
118
  if schema.type == "float":
63
119
  double_spin_box = QtWidgets.QDoubleSpinBox()
@@ -69,7 +125,11 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
69
125
  double_spin_box.setValue(float(schema.default))
70
126
  double_spin_box.valueChanged.connect(lambda _: on_change())
71
127
  return _FieldEditor(
72
- double_spin_box, double_spin_box.value, lambda value: double_spin_box.setValue(float(cast("float", value)))
128
+ double_spin_box,
129
+ double_spin_box.value,
130
+ lambda value: double_spin_box.setValue(float(cast("float", value))),
131
+ level=schema.level,
132
+ row_widget=_wrap_with_description(double_spin_box, schema.description),
73
133
  )
74
134
 
75
135
  if schema.type == "bool":
@@ -77,7 +137,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
77
137
  if schema.default is not None:
78
138
  check_box.setChecked(bool(schema.default))
79
139
  check_box.toggled.connect(lambda _: on_change())
80
- return _FieldEditor(check_box, check_box.isChecked, lambda value: check_box.setChecked(bool(value)))
140
+ return _FieldEditor(
141
+ check_box,
142
+ check_box.isChecked,
143
+ lambda value: check_box.setChecked(bool(value)),
144
+ level=schema.level,
145
+ row_widget=_wrap_with_description(check_box, schema.description),
146
+ )
81
147
 
82
148
  if schema.type == "enum":
83
149
  combo_box = QtWidgets.QComboBox()
@@ -86,7 +152,13 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
86
152
  if schema.default is not None and str(schema.default) in options:
87
153
  combo_box.setCurrentText(str(schema.default))
88
154
  combo_box.currentTextChanged.connect(lambda _: on_change())
89
- return _FieldEditor(combo_box, combo_box.currentText, lambda value: combo_box.setCurrentText(str(value)))
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
+ )
90
162
 
91
163
  if schema.type == "object" and schema.nested is not None:
92
164
  group_box = QtWidgets.QGroupBox(name)
@@ -94,14 +166,20 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
94
166
  group_box.setLayout(form_layout)
95
167
  children: Dict[str, _FieldEditor] = {}
96
168
  for child_name, child_schema in schema.nested.items():
169
+ if child_schema.level == AccessLevel.HIDDEN:
170
+ continue
97
171
  child_editor = _build_editor(child_name, child_schema, on_change)
98
- form_layout.addRow(child_name, child_editor.widget)
172
+ child_label = QtWidgets.QLabel(child_name)
173
+ form_layout.addRow(child_label, child_editor.display_widget)
174
+ child_editor.row_label = child_label
99
175
  children[child_name] = child_editor
100
176
  return _FieldEditor(
101
177
  group_box,
102
178
  lambda: _nested_get(children),
103
179
  lambda value: _nested_set(children, value),
104
180
  children=children,
181
+ level=schema.level,
182
+ row_widget=_wrap_with_description(group_box, schema.description),
105
183
  )
106
184
 
107
185
  # object without a nested schema (pydantic freeform dict), or any other schema type this
@@ -120,7 +198,11 @@ def _build_editor(name: str, schema: ConfigFieldSchema, on_change: Callable[[],
120
198
  placeholder.setReadOnly(True)
121
199
  placeholder.setEnabled(False)
122
200
  placeholder.setToolTip(f"Field type {schema.type!r} has no schema-driven editor for this widget.")
123
- return _FieldEditor(placeholder, _get_raw, _set_raw)
201
+ # always BASIC regardless of schema.level: this is a loud "unsupported type" flag, meant to
202
+ # stay visible so a developer notices it, never swept into the expert-toggle's hide group
203
+ return _FieldEditor(
204
+ placeholder, _get_raw, _set_raw, row_widget=_wrap_with_description(placeholder, schema.description)
205
+ )
124
206
 
125
207
 
126
208
  class StructuredConfigWidget(BaseWidget):
@@ -142,10 +224,20 @@ class StructuredConfigWidget(BaseWidget):
142
224
  # itself register as a user edit (which would otherwise re-enable Apply/Reset for a
143
225
  # change that came from the module, not from the operator)
144
226
  self._applying_state = False
227
+ # every EXPERT-level editor across the whole (possibly nested) form, collected once in
228
+ # _build_form; their rows (label + widget) are shown/hidden together by checkBoxAdvanced.
229
+ # No password gate, unlike pyftscontrol's Expert Mode button -- operators here are
230
+ # already authenticated via the comm layer, so it's just a visibility toggle, not access
231
+ # control.
232
+ self._expert_editors: List[_FieldEditor] = []
145
233
 
146
234
  outer_layout = QtWidgets.QVBoxLayout()
147
235
  self.setLayout(outer_layout)
148
236
 
237
+ self.checkBoxAdvanced = QtWidgets.QCheckBox("Show advanced fields")
238
+ self.checkBoxAdvanced.toggled.connect(self._on_advanced_toggled)
239
+ outer_layout.addWidget(self.checkBoxAdvanced)
240
+
149
241
  scroll_area = QtWidgets.QScrollArea()
150
242
  scroll_area.setWidgetResizable(True)
151
243
  self._form_container = QtWidgets.QWidget()
@@ -186,8 +278,12 @@ class StructuredConfigWidget(BaseWidget):
186
278
  def _build_form(self, schema: ConfigSchema) -> None:
187
279
  children: Dict[str, _FieldEditor] = {}
188
280
  for name, field_schema in schema.fields.items():
281
+ if field_schema.level == AccessLevel.HIDDEN:
282
+ continue
189
283
  editor = _build_editor(name, field_schema, self._on_editor_changed)
190
- self._form_layout.addRow(name, editor.widget)
284
+ label = QtWidgets.QLabel(name)
285
+ self._form_layout.addRow(label, editor.display_widget)
286
+ editor.row_label = label
191
287
  children[name] = editor
192
288
  self._root = _FieldEditor(
193
289
  self._form_container,
@@ -195,11 +291,19 @@ class StructuredConfigWidget(BaseWidget):
195
291
  lambda value: _nested_set(children, value),
196
292
  children=children,
197
293
  )
294
+ self._expert_editors = _collect_by_level(self._root, AccessLevel.EXPERT)
295
+ self._on_advanced_toggled(self.checkBoxAdvanced.isChecked())
198
296
  # a state may already have arrived (and been cached) before the form existed to receive
199
297
  # it -- apply it now instead of waiting for the next update
200
298
  if self._last_applied is not None:
201
299
  self.signal_update_gui.emit()
202
300
 
301
+ def _on_advanced_toggled(self, checked: bool) -> None:
302
+ for editor in self._expert_editors:
303
+ editor.display_widget.setVisible(checked)
304
+ if editor.row_label is not None:
305
+ editor.row_label.setVisible(checked)
306
+
203
307
  def _on_state(self, state: ConfigAppliedState) -> None:
204
308
  self._last_applied = dict(state.config)
205
309
  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.4.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"
@@ -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,131 @@ 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
+ # _build_editor itself only *tags* each editor with its level; it doesn't touch visibility --
158
+ # that's StructuredConfigWidget's job (_build_form/_on_advanced_toggled), tested below.
159
+
160
+
161
+ def test_editor_is_tagged_with_its_schema_level() -> None:
162
+ basic = _build_editor("count", ConfigFieldSchema(type="int", default=3, level=AccessLevel.BASIC), lambda: None)
163
+ expert = _build_editor("count", ConfigFieldSchema(type="int", default=3, level=AccessLevel.EXPERT), lambda: None)
164
+ assert basic.level == AccessLevel.BASIC
165
+ assert expert.level == AccessLevel.EXPERT
166
+
167
+
168
+ def test_unsupported_type_placeholder_ignores_expert_level_for_toggle_grouping() -> None:
169
+ """A permanently non-editable placeholder must never be swept into the expert-toggle's
170
+ hide/show group, even if the underlying field is tagged EXPERT -- see the comment in
171
+ _build_editor's fallback branch."""
172
+ editor = _build_editor(
173
+ "items", ConfigFieldSchema(type="list", default=[1, 2], level=AccessLevel.EXPERT), lambda: None
174
+ )
175
+ assert editor.level == AccessLevel.BASIC
176
+ assert editor.widget.isEnabled() is False
177
+
178
+
179
+ def test_nested_object_skips_hidden_children() -> None:
180
+ schema = ConfigFieldSchema(
181
+ type="object",
182
+ level=AccessLevel.EXPERT,
183
+ nested={
184
+ "az": ConfigFieldSchema(type="float", default=1.0),
185
+ "secret": ConfigFieldSchema(type="float", default=2.0, level=AccessLevel.HIDDEN),
186
+ },
187
+ )
188
+ editor = _build_editor("pointing", schema, lambda: None)
189
+ assert editor.children is not None and set(editor.children) == {"az"}
190
+ assert editor.level == AccessLevel.EXPERT
191
+
192
+
193
+ def _sample_schema_with_levels() -> ConfigSchema:
194
+ return ConfigSchema(
195
+ fields={
196
+ "name": ConfigFieldSchema(type="str", default="foo", level=AccessLevel.BASIC),
197
+ "gain": ConfigFieldSchema(type="int", default=1, level=AccessLevel.EXPERT),
198
+ "secret": ConfigFieldSchema(type="int", default=0, level=AccessLevel.HIDDEN),
199
+ }
200
+ )
201
+
202
+
203
+ def test_build_form_skips_hidden_fields_entirely() -> None:
204
+ widget = StructuredConfigWidget()
205
+ widget._build_form(_sample_schema_with_levels())
206
+
207
+ assert widget._form_layout.rowCount() == 2 # name + gain, not secret
208
+ assert widget._root is not None and widget._root.children is not None
209
+ assert set(widget._root.children) == {"name", "gain"}
210
+ widget.close()
211
+
212
+
213
+ def test_expert_field_row_starts_hidden_basic_field_row_does_not() -> None:
214
+ widget = StructuredConfigWidget()
215
+ widget._build_form(_sample_schema_with_levels())
216
+ assert widget._root is not None and widget._root.children is not None
217
+ basic_editor = widget._root.children["name"]
218
+ expert_editor = widget._root.children["gain"]
219
+
220
+ assert basic_editor.widget.isHidden() is False
221
+ assert basic_editor.row_label is not None and basic_editor.row_label.isHidden() is False
222
+ assert expert_editor.widget.isHidden() is True
223
+ assert expert_editor.row_label is not None and expert_editor.row_label.isHidden() is True
224
+ widget.close()
225
+
226
+
227
+ def test_advanced_checkbox_shows_and_hides_expert_field_row() -> None:
228
+ widget = StructuredConfigWidget()
229
+ widget._build_form(_sample_schema_with_levels())
230
+ assert widget._root is not None and widget._root.children is not None
231
+ expert_editor = widget._root.children["gain"]
232
+
233
+ widget.checkBoxAdvanced.setChecked(True)
234
+ assert expert_editor.widget.isHidden() is False
235
+ assert expert_editor.row_label is not None and expert_editor.row_label.isHidden() is False
236
+
237
+ widget.checkBoxAdvanced.setChecked(False)
238
+ assert expert_editor.widget.isHidden() is True
239
+ assert expert_editor.row_label is not None and expert_editor.row_label.isHidden() is True
240
+
241
+
242
+ # -- _build_editor: description rendered below the widget -----------------------------------
243
+
244
+
245
+ def test_field_without_description_is_not_wrapped() -> None:
246
+ editor = _build_editor("count", ConfigFieldSchema(type="int", default=3), lambda: None)
247
+ assert editor.row_widget is None
248
+ assert editor.display_widget is editor.widget
249
+
250
+
251
+ def test_field_with_description_wraps_widget_with_label_below() -> None:
252
+ schema = ConfigFieldSchema(type="int", default=3, description="Number of exposures to take")
253
+ editor = _build_editor("count", schema, lambda: None)
254
+
255
+ assert editor.row_widget is not None
256
+ assert editor.display_widget is editor.row_widget
257
+ container_layout = editor.row_widget.layout()
258
+ assert container_layout.count() == 2
259
+ assert container_layout.itemAt(0).widget() is editor.widget
260
+ description_label = container_layout.itemAt(1).widget()
261
+ assert isinstance(description_label, QtWidgets.QLabel)
262
+ assert description_label.text() == "Number of exposures to take"
263
+
264
+
265
+ def test_build_form_places_description_wrapper_in_the_row() -> None:
266
+ widget = StructuredConfigWidget()
267
+ schema = ConfigSchema(
268
+ fields={"count": ConfigFieldSchema(type="int", default=3, description="Number of exposures to take")}
269
+ )
270
+ widget._build_form(schema)
271
+
272
+ assert widget._root is not None and widget._root.children is not None
273
+ editor = widget._root.children["count"]
274
+ row_field_item = widget._form_layout.itemAt(0, QtWidgets.QFormLayout.ItemRole.FieldRole)
275
+ assert row_field_item.widget() is editor.row_widget
276
+ widget.close()
277
+ widget.close()
278
+
279
+
155
280
  # -- StructuredConfigWidget: state population, dirty tracking, apply/reset ------------------
156
281
 
157
282
 
@@ -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.4.0"
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