pyobs-gui 2.4.2__tar.gz → 2.5.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 (174) hide show
  1. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/PKG-INFO +1 -1
  2. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/spectrographwidget.py +41 -10
  3. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyproject.toml +1 -1
  4. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/2026-09-14-fitswidget-toolbar-overflow.md +3 -2
  5. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/index.md +10 -7
  6. pyobs_gui-2.5.1/tests/test_spectrographwidget.py +117 -0
  7. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/uv.lock +28 -28
  8. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.claude/skills/verify/SKILL.md +0 -0
  9. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.github/dependabot.yml +0 -0
  10. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.github/workflows/dependabot-automerge.yml +0 -0
  11. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.github/workflows/pypi.yml +0 -0
  12. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.github/workflows/pyrefly.yml +0 -0
  13. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.github/workflows/ruff.yml +0 -0
  14. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.gitignore +0 -0
  15. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.pre-commit-config.yaml +0 -0
  16. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/.readthedocs.yml +0 -0
  17. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/CHANGELOG.rst +0 -0
  18. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/CLAUDE.md +0 -0
  19. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/LICENSE +0 -0
  20. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/README.md +0 -0
  21. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/bin/mypyuic5 +0 -0
  22. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/docs/Makefile +0 -0
  23. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/docs/make.bat +0 -0
  24. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/docs/requirements.txt +0 -0
  25. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/docs/source/_static/pyobs.gif +0 -0
  26. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/docs/source/conf.py +0 -0
  27. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/docs/source/index.rst +0 -0
  28. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/mypy.ini +0 -0
  29. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/__init__.py +0 -0
  30. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/__main__.py +0 -0
  31. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
  32. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/accounts.py +0 -0
  33. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/acquisitionwidget.py +0 -0
  34. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/autofocuswidget.py +0 -0
  35. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/autoguidingwidget.py +0 -0
  36. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/base.py +0 -0
  37. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/camerawidget.py +0 -0
  38. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/commandinputwidget.py +0 -0
  39. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/compassmovewidget.py +0 -0
  40. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/coolingwidget.py +0 -0
  41. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/datadisplaywidget.py +0 -0
  42. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/eventswidget.py +0 -0
  43. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/filterwidget.py +0 -0
  44. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/fitsheaderswidget.py +0 -0
  45. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/focuswidget.py +0 -0
  46. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/gui.py +0 -0
  47. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/login.py +0 -0
  48. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/loginwindow.py +0 -0
  49. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/logmodel.py +0 -0
  50. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/mainwindow.py +0 -0
  51. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/modewidget.py +0 -0
  52. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/modifiedmixin.py +0 -0
  53. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/modulegui.py +0 -0
  54. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/movestack.py +0 -0
  55. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/nowheelfilter.py +0 -0
  56. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/__init__.py +0 -0
  57. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  58. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  59. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  60. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  61. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  62. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  63. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/camerawidget.ui +0 -0
  64. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  65. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  66. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/compassmovewidget.py +0 -0
  67. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  68. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  69. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/compile.sh +0 -0
  70. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/coolingwidget.ui +0 -0
  71. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  72. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  73. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  74. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  75. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/designer.sh +0 -0
  76. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/eventswidget.ui +0 -0
  77. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  78. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/filterwidget.ui +0 -0
  79. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  80. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  81. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  82. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/focuswidget.ui +0 -0
  83. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  84. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/mainwindow.ui +0 -0
  85. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  86. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/modewidget.ui +0 -0
  87. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/modewidget_ui.py +0 -0
  88. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  89. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  90. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  91. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  92. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  93. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  94. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  95. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  96. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  97. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  98. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  99. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  100. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources.qrc +0 -0
  101. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/resources_rc.py +0 -0
  102. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/roboticwidget.ui +0 -0
  103. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
  104. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/roofwidget.ui +0 -0
  105. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  106. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/schedulewidget.ui +0 -0
  107. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
  108. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/shellwidget.ui +0 -0
  109. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  110. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  111. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  112. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/telescopewidget.ui +0 -0
  113. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  114. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  115. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  116. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  117. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  118. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/videograbwidget.ui +0 -0
  119. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/videograbwidget_ui.py +0 -0
  120. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/videowidget.ui +0 -0
  121. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/videowidget_ui.py +0 -0
  122. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/weatherwidget.ui +0 -0
  123. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  124. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/roboticwidget.py +0 -0
  125. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/roofwidget.py +0 -0
  126. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/schedulewidget.py +0 -0
  127. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/shellwidget.py +0 -0
  128. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/statuswidget.py +0 -0
  129. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/structuredconfigwidget.py +0 -0
  130. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/telescopewidget.py +0 -0
  131. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/temperaturesplotwidget.py +0 -0
  132. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/temperatureswidget.py +0 -0
  133. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/utils.py +0 -0
  134. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/videograbwidget.py +0 -0
  135. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/videowidget.py +0 -0
  136. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/watchedlabel.py +0 -0
  137. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pyobs_gui/weatherwidget.py +0 -0
  138. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/pysidedeploy.spec +0 -0
  139. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  140. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
  141. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +0 -0
  142. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/2026-08-28-structuredconfig-widget.md +0 -0
  143. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/2026-08-31-irobotic-widgets.md +0 -0
  144. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/2026-09-01-gui-video-widget-split.md +0 -0
  145. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/specs/2026-09-14-stacked-widget-scroll-fallback.md +0 -0
  146. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/spike_standalone/main.py +0 -0
  147. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/spike_standalone/pysidedeploy.spec +0 -0
  148. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/acquisition.yaml +0 -0
  149. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/autofocus.yaml +0 -0
  150. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/camera.yaml +0 -0
  151. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/full.yaml +0 -0
  152. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/guiding.yaml +0 -0
  153. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/mode.yaml +0 -0
  154. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/robotic.yaml +0 -0
  155. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/roof.yaml +0 -0
  156. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/spectrograph.yaml +0 -0
  157. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/structuredconfig.yaml +0 -0
  158. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/telescope.yaml +0 -0
  159. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/telescope_acl.yaml +0 -0
  160. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/telescope_acl_denied.yaml +0 -0
  161. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/test/video.yaml +0 -0
  162. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/__init__.py +0 -0
  163. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/conftest.py +0 -0
  164. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_accounts.py +0 -0
  165. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_camerawidget.py +0 -0
  166. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_loginwindow.py +0 -0
  167. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_mainwindow_startup.py +0 -0
  168. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_modewidget.py +0 -0
  169. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_multiwidget_pages.py +0 -0
  170. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_roboticwidget.py +0 -0
  171. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_schedulewidget.py +0 -0
  172. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_structuredconfigwidget.py +0 -0
  173. {pyobs_gui-2.4.2 → pyobs_gui-2.5.1}/tests/test_videograbwidget.py +0 -0
  174. {pyobs_gui-2.4.2 → pyobs_gui-2.5.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.4.2
3
+ Version: 2.5.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
@@ -4,7 +4,7 @@ import logging
4
4
  from typing import Any, TYPE_CHECKING
5
5
  from PySide6 import QtCore # type: ignore
6
6
 
7
- from pyobs.interfaces import IAbortable, IExposure, ExposureState
7
+ from pyobs.interfaces import IAbortable, IExposure, ExposureState, IDataSequence, DataSequenceState
8
8
  from pyobs.utils.enums import ExposureStatus
9
9
  from .base import BaseWidget
10
10
 
@@ -53,34 +53,65 @@ class SpectrographWidget(BaseWidget, Ui_SpectrographWidget):
53
53
 
54
54
  async def _init(self) -> None:
55
55
  await self.comm.subscribe_state(self.module, IExposure, self._on_exposure_state)
56
+ if await self.comm.has_proxy(self.module, IDataSequence):
57
+ await self.comm.subscribe_state(self.module, IDataSequence, self._on_sequence_state)
56
58
 
57
59
  def _on_exposure_state(self, state: ExposureState) -> None:
58
60
  self.exposure_status = state.status
59
61
  self.signal_update_gui.emit()
60
62
 
63
+ def _on_sequence_state(self, state: DataSequenceState) -> None:
64
+ self.exposures_left = state.count_left
65
+ self.signal_update_gui.emit()
66
+
61
67
  def grab_spectrum(self) -> None:
62
68
  self.run_background(self._grab_spectra)
63
69
 
64
70
  async def _grab_spectra(self) -> None:
65
71
  broadcast = self.checkBroadcast.isChecked()
66
72
 
67
- # take the requested number of spectra; an exception (e.g. from a failed exposure,
68
- # or abort() zeroing exposures_left mid-loop) stops the sequence
69
73
  self.exposures_left = self.spinCount.value()
70
74
  self.signal_update_gui.emit()
75
+
76
+ # if the module can grab a counted sequence server-side, let it -- but only when
77
+ # broadcasting, since grab_sequence() doesn't hand filenames back to the caller and
78
+ # the client has no other way to learn a spectrum is ready to display
79
+ if broadcast:
80
+ async with self.comm.safe_proxy(self.module, IDataSequence) as proxy:
81
+ if proxy is not None:
82
+ await proxy.grab_sequence(self.exposures_left, broadcast)
83
+ return
84
+
85
+ # fall back to a client-side loop for modules that don't support IDataSequence, or
86
+ # when not broadcasting (grab_data() returns the filename directly for display); an
87
+ # exception (e.g. from a failed exposure, or abort() zeroing exposures_left mid-loop)
88
+ # stops the sequence
71
89
  while self.exposures_left > 0:
72
90
  await self.datadisplay.grab_data(broadcast)
73
91
  self.exposures_left -= 1
74
92
  self.signal_update_gui.emit()
75
93
 
76
94
  def abort(self) -> None:
77
- # stop the sequence after the current spectrum finishes aborting
78
- self.exposures_left = 0
79
- self.run_background(self._abort_sequence)
80
-
81
- async def _abort_sequence(self) -> None:
82
- async with self.comm.proxy(self.module, IAbortable) as proxy:
83
- await proxy.abort()
95
+ # do we have a running sequence?
96
+ if self.exposures_left == 0:
97
+ return
98
+
99
+ self.run_background(self._do_abort)
100
+
101
+ async def _do_abort(self) -> None:
102
+ # got spectra left?
103
+ if self.exposures_left > 1:
104
+ # soft-stop the sequence server-side (current spectrum finishes normally), if
105
+ # supported; otherwise just stop the client-side loop after the current spectrum
106
+ async with self.comm.safe_proxy(self.module, IDataSequence) as proxy:
107
+ if proxy is not None:
108
+ await proxy.abort_sequence()
109
+ return
110
+ self.exposures_left = 0
111
+ else:
112
+ async with self.comm.safe_proxy(self.module, IAbortable) as proxy:
113
+ if proxy is not None:
114
+ await proxy.abort()
84
115
 
85
116
  def update_gui(self) -> None:
86
117
  self.setEnabled(True)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-gui"
3
- version = "2.4.2"
3
+ version = "2.5.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"
@@ -1,7 +1,8 @@
1
1
  # Plan: qfitswidget — responsive Cuts/Stretch/Colormap toolbar (hide + overflow, not wrap)
2
2
 
3
- Status: implemented (qfitswidget `develop`, 2026-09-14) — see "Implementation notes" below for
4
- three real bugs found and fixed during verification, and one design change from what this doc
3
+ Status: implemented, closed — released in qfitswidget v1.1.3, `pyobs-gui`'s floor bumped to match
4
+ and released in v2.4.2. See "Implementation notes" below for the full list of real bugs found and
5
+ fixed during live verification (eight, not three), and one design change from what this doc
5
6
  originally proposed (thresholds are measured at runtime, not hardcoded).
6
7
 
7
8
  Repos: qfitswidget (all implementation here; surfaced from pyobs-gui work, see "Context" below)
@@ -13,13 +13,16 @@ ADRs that concern `pyobs-gui` live in `pyobs-core`'s `specs/` tree instead (`spe
13
13
  further, instead of every such floor needing its own individual fix. The flagged
14
14
  mouse-wheel-over-spinbox papercut was confirmed real (not just theoretical) and fixed via
15
15
  `nowheelfilter.py`'s app-wide event filter.
16
- - `2026-09-14-fitswidget-toolbar-overflow.md` — **implemented**. Repos: qfitswidget (hosted here
17
- since qfitswidget has no `specs/` of its own). Responsive Cuts/Stretch/Colormap toolbar in
18
- `QFitsWidget`: hide-then-overflow (not wrap) as width shrinks, `QMenu`/`QWidgetAction` overflow
19
- for the two checkboxes, manual-cuts fields hidden outright whenever Cuts ≠ Manual. Thresholds
20
- measured at runtime rather than hardcoded; three real bugs found via testing (dangling
21
- `QWidgetAction` widget, reparent-hides-widget, `self.width()` vs `event.size()`) — see the plan
22
- doc's "Implementation notes".
16
+ - `2026-09-14-fitswidget-toolbar-overflow.md` — **implemented, closed**. Repos: qfitswidget
17
+ (hosted here since qfitswidget has no `specs/` of its own). Responsive Cuts/Stretch/Colormap
18
+ toolbar in `QFitsWidget`: hide-then-overflow (not wrap) as width shrinks, `QMenu`/`QWidgetAction`
19
+ overflow for the two checkboxes, manual-cuts fields hidden outright whenever Cuts ≠ Manual.
20
+ Released in qfitswidget v1.1.3; this repo's floor bumped and released in v2.4.2. Eight real bugs
21
+ found via live testing, not just the isolated headless tests — see the plan doc's
22
+ "Implementation notes" for the full list (a chicken-and-egg minimumSizeHint deadlock, missing
23
+ hysteresis, a `uv run` auto-sync trap that silently discarded an editable install across several
24
+ rounds of "still broken" reports, a `QWidgetAction` deleteLater() crash, and a one-tier-deep
25
+ repeat of the minimumSizeHint bug).
23
26
  - `2026-08-31-irobotic-widgets.md` — **implemented, closed (issue #825, PR #155, `5794186`)**.
24
27
  `RoboticWidget` / `ScheduleWidget` for `IRobotic`/`IRoboticScheduler`.
25
28
  - `2026-08-28-structuredconfig-widget.md` — **proposed (issue #154)**. Generic
@@ -0,0 +1,117 @@
1
+ from types import SimpleNamespace
2
+ from unittest.mock import AsyncMock
3
+
4
+ import pytest
5
+
6
+ from pyobs.interfaces import IAbortable, IDataSequence
7
+ from pyobs_gui.spectrographwidget import SpectrographWidget
8
+
9
+
10
+ class _AsyncProxyCM:
11
+ """Mimics Comm.safe_proxy()'s async context manager, yielding a fixed proxy (or None)."""
12
+
13
+ def __init__(self, value):
14
+ self._value = value
15
+
16
+ async def __aenter__(self):
17
+ return self._value
18
+
19
+ async def __aexit__(self, *exc_info):
20
+ return False
21
+
22
+
23
+ class FakeComm:
24
+ """Only implements what SpectrographWidget._grab_spectra()/_do_abort() touch; an interface
25
+ with no proxy given resolves to "not supported" (proxy is None)."""
26
+
27
+ def __init__(self, data_sequence_proxy=None, abortable_proxy=None):
28
+ self._data_sequence_proxy = data_sequence_proxy
29
+ self._abortable_proxy = abortable_proxy
30
+
31
+ def safe_proxy(self, module, interface):
32
+ if interface is IDataSequence:
33
+ value = self._data_sequence_proxy
34
+ elif interface is IAbortable:
35
+ value = self._abortable_proxy
36
+ else:
37
+ value = None
38
+ return _AsyncProxyCM(value)
39
+
40
+
41
+ def make_widget(data_sequence_proxy=None, abortable_proxy=None) -> SpectrographWidget:
42
+ widget = SpectrographWidget()
43
+ widget.modules = ["spectrograph"]
44
+ widget._comm = FakeComm( # pyrefly: ignore [bad-assignment]
45
+ data_sequence_proxy=data_sequence_proxy, abortable_proxy=abortable_proxy
46
+ )
47
+ widget.datadisplay.grab_data = AsyncMock()
48
+ return widget
49
+
50
+
51
+ @pytest.mark.asyncio
52
+ async def test_grab_spectra_uses_server_side_sequence_when_broadcasting(qapp) -> None:
53
+ proxy = SimpleNamespace(grab_sequence=AsyncMock())
54
+ widget = make_widget(proxy)
55
+ widget.checkBroadcast.setChecked(True)
56
+ widget.spinCount.setValue(3)
57
+
58
+ await widget._grab_spectra()
59
+
60
+ proxy.grab_sequence.assert_awaited_once_with(3, True)
61
+ widget.datadisplay.grab_data.assert_not_called()
62
+ widget.close()
63
+
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_grab_spectra_falls_back_to_client_side_loop_when_not_broadcasting(qapp) -> None:
67
+ """Without broadcasting, grab_sequence() has no way to hand a filename back to the caller,
68
+ so the GUI would have no way to learn a spectrum is ready and nothing would ever be
69
+ displayed -- same reasoning as CameraWidget's equivalent regression test (issue #122)."""
70
+ proxy = SimpleNamespace(grab_sequence=AsyncMock())
71
+ widget = make_widget(proxy)
72
+ widget.checkBroadcast.setChecked(False)
73
+ widget.spinCount.setValue(2)
74
+
75
+ await widget._grab_spectra()
76
+
77
+ proxy.grab_sequence.assert_not_called()
78
+ assert widget.datadisplay.grab_data.await_count == 2
79
+ widget.datadisplay.grab_data.assert_awaited_with(False)
80
+ widget.close()
81
+
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_grab_spectra_falls_back_when_module_has_no_data_sequence_support(qapp) -> None:
85
+ widget = make_widget(data_sequence_proxy=None)
86
+ widget.checkBroadcast.setChecked(True)
87
+ widget.spinCount.setValue(1)
88
+
89
+ await widget._grab_spectra()
90
+
91
+ assert widget.datadisplay.grab_data.await_count == 1
92
+ widget.datadisplay.grab_data.assert_awaited_with(True)
93
+ widget.close()
94
+
95
+
96
+ @pytest.mark.asyncio
97
+ async def test_abort_soft_stops_server_side_sequence_when_more_than_one_left(qapp) -> None:
98
+ proxy = SimpleNamespace(abort_sequence=AsyncMock())
99
+ widget = make_widget(proxy)
100
+ widget.exposures_left = 2
101
+
102
+ await widget._do_abort()
103
+
104
+ proxy.abort_sequence.assert_awaited_once()
105
+ widget.close()
106
+
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_abort_hard_aborts_when_one_left(qapp) -> None:
110
+ proxy = SimpleNamespace(abort=AsyncMock())
111
+ widget = make_widget(abortable_proxy=proxy)
112
+ widget.exposures_left = 1
113
+
114
+ await widget._do_abort()
115
+
116
+ proxy.abort.assert_awaited_once()
117
+ widget.close()
@@ -2623,15 +2623,15 @@ wheels = [
2623
2623
 
2624
2624
  [[package]]
2625
2625
  name = "pandas-stubs"
2626
- version = "3.0.5.260730"
2626
+ version = "3.0.5.260914"
2627
2627
  source = { registry = "https://pypi.org/simple" }
2628
2628
  dependencies = [
2629
2629
  { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
2630
2630
  { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
2631
2631
  ]
2632
- sdist = { url = "https://files.pythonhosted.org/packages/c2/d2/dea4a3a56b7b5f69c5fbca9f14625fcf28e1a39a657e9833d4a10bcac593/pandas_stubs-3.0.5.260730.tar.gz", hash = "sha256:f70a232c57d93a5a2c81f8a53953e10891a5374bc92652277deb325e2e4d0ff3", size = 114631, upload-time = "2026-07-30T14:31:42.271Z" }
2632
+ sdist = { url = "https://files.pythonhosted.org/packages/c1/93/8948ae6c1e1e3d6833596fd266f7be2d27c1451b8be094975ad42c5e842e/pandas_stubs-3.0.5.260914.tar.gz", hash = "sha256:3f6fc1f147f68fd89c007105e7c94a948acb4ecd7eb20dc1c02e153c4ed5c250", size = 117622, upload-time = "2026-09-14T16:42:35.065Z" }
2633
2633
  wheels = [
2634
- { url = "https://files.pythonhosted.org/packages/60/c2/959caec5c46f484b5f8bb6def4b0cf7a45ba6acda26f12b75142d98cc5ae/pandas_stubs-3.0.5.260730-py3-none-any.whl", hash = "sha256:60e90e3e1eda6937e337e243cbe6217e151c11137cd7eddf832af537c7310bfd", size = 174807, upload-time = "2026-07-30T14:31:41.17Z" },
2634
+ { url = "https://files.pythonhosted.org/packages/9a/cb/5ad79e02a556cc23fed5816de0109fa8af660c66cfa5f4af74c3e8d4cd26/pandas_stubs-3.0.5.260914-py3-none-any.whl", hash = "sha256:39a1300c5c5c55fdf609e3476805decce5d5015539a4dcb683449f8feaeee2fb", size = 177344, upload-time = "2026-09-14T16:42:33.771Z" },
2635
2635
  ]
2636
2636
 
2637
2637
  [[package]]
@@ -3263,7 +3263,7 @@ wheels = [
3263
3263
 
3264
3264
  [[package]]
3265
3265
  name = "pyobs-core"
3266
- version = "2.8.7"
3266
+ version = "2.10.3"
3267
3267
  source = { registry = "https://pypi.org/simple" }
3268
3268
  dependencies = [
3269
3269
  { name = "aiohttp" },
@@ -3295,14 +3295,14 @@ dependencies = [
3295
3295
  { name = "tenacity" },
3296
3296
  { name = "typing-extensions" },
3297
3297
  ]
3298
- sdist = { url = "https://files.pythonhosted.org/packages/fd/74/8e30782e34d412e069a6c38393a7e347477294f6c3dae470dcac9aa03783/pyobs_core-2.8.7.tar.gz", hash = "sha256:001751b076fc3af72baac28d600ccee2fdb5a3d3cad38bbf1cae36832aa8cbde", size = 411818, upload-time = "2026-09-09T12:31:51.561Z" }
3298
+ sdist = { url = "https://files.pythonhosted.org/packages/b8/3a/2c3d76a1b8ca96f689d34da610d96325292fdaac9df0c7d34451f2cb30c1/pyobs_core-2.10.3.tar.gz", hash = "sha256:27c965f626b6ef2502eda630da6841e7977ad29c65b6299a0403a4ed22eb7e12", size = 420378, upload-time = "2026-09-18T09:15:10.904Z" }
3299
3299
  wheels = [
3300
- { url = "https://files.pythonhosted.org/packages/df/0b/b8a4ef5652f95b26a94567b0f2bb59ab9b304f612059eb216031b8ad0ff6/pyobs_core-2.8.7-py3-none-any.whl", hash = "sha256:ea8ad9d5fca01585977e31e7ab66b2978ea24ea895d69610ba857647dafac096", size = 641759, upload-time = "2026-09-09T12:31:49.673Z" },
3300
+ { url = "https://files.pythonhosted.org/packages/15/6b/0471ea25d46df94cc7c65d67e35ed52a851432978dc58a69bda8ec58bca9/pyobs_core-2.10.3-py3-none-any.whl", hash = "sha256:4ff3c86e23a86ab41541aedb25fa17e46bab832822097adccdb503de76454feb", size = 652925, upload-time = "2026-09-18T09:15:09.374Z" },
3301
3301
  ]
3302
3302
 
3303
3303
  [[package]]
3304
3304
  name = "pyobs-gui"
3305
- version = "2.4.2"
3305
+ version = "2.5.1"
3306
3306
  source = { editable = "." }
3307
3307
  dependencies = [
3308
3308
  { name = "astroplan" },
@@ -3788,27 +3788,27 @@ wheels = [
3788
3788
 
3789
3789
  [[package]]
3790
3790
  name = "ruff"
3791
- version = "0.16.7"
3792
- source = { registry = "https://pypi.org/simple" }
3793
- sdist = { url = "https://files.pythonhosted.org/packages/82/bb/5a449b9162e49b139d72f61672bd3ac1d790221f796d3304e2241fff4c58/ruff-0.16.7.tar.gz", hash = "sha256:5f71d004ac1263b22fa39462ac5ae618a4b77d58981af2cc79bf79a29c12b1a6", size = 4924184, upload-time = "2026-09-10T18:04:06.336Z" }
3794
- wheels = [
3795
- { url = "https://files.pythonhosted.org/packages/e3/b2/c80aeeb7f9e469c0d63a85d2f1ab6e1ebfbe10ea7a8d2438b7e09e3ff09e/ruff-0.16.7-py3-none-linux_armv6l.whl", hash = "sha256:727307773e7c7f9181d3ed3a2484186e56c1fa1874255911c74585eb2c7c19f9", size = 10048917, upload-time = "2026-09-10T18:03:30.28Z" },
3796
- { url = "https://files.pythonhosted.org/packages/7b/96/20bb7bcae008004df52afcb7ac83432d4a467f2c17b672fe46d26be231c5/ruff-0.16.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9d61c258deabf58f34c67bd4bb4d939c7f2e6b5f0e59c1cdd1cf771b11cde929", size = 10242929, upload-time = "2026-09-10T18:03:32.706Z" },
3797
- { url = "https://files.pythonhosted.org/packages/90/b2/f184b0d5abec02db69cfd7e49b688ae0237554528ca777136c613bf36bee/ruff-0.16.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ab81118df8945e0193d0240712aa4496573595b75185c3636ed825592a0f728", size = 9847245, upload-time = "2026-09-10T18:03:34.509Z" },
3798
- { url = "https://files.pythonhosted.org/packages/eb/2d/db1633a641866ed801e34cc6b60ef236c5e16f9b2124ab1d49cc24a5fe4f/ruff-0.16.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c196c968874fc8019da8e7163de7a1a370f111e2309b4b7dfea0fce950198d0", size = 9961780, upload-time = "2026-09-10T18:03:36.618Z" },
3799
- { url = "https://files.pythonhosted.org/packages/4d/98/edea21e1a3e38dbbc3bf6bb068b863b3b06184cf8533a4c7dbbe208a89d5/ruff-0.16.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac8c3bd0a7e10ad31e6ce51e7a99f3cb772e69aecdd6b9ea7e99b362f62a62c0", size = 9866337, upload-time = "2026-09-10T18:03:38.805Z" },
3800
- { url = "https://files.pythonhosted.org/packages/0b/11/a15e60d4c87b214646f116ca9d204475bf993ee1047459bc9a360fd4d6d1/ruff-0.16.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d3988edde000b5c75dc1b3f584708da9bc990de069c18909142580fec1af9", size = 10562512, upload-time = "2026-09-10T18:03:40.71Z" },
3801
- { url = "https://files.pythonhosted.org/packages/29/42/eaff4c9b6d0c7cdf56df313a17e89ae854f5bbc0b0c8f9cce19be0ab7a8f/ruff-0.16.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce05b62b770a8217c4646a9c4139fca00efe8fe5d71f87df2b243ff20d4584d1", size = 11302938, upload-time = "2026-09-10T18:03:42.607Z" },
3802
- { url = "https://files.pythonhosted.org/packages/5d/43/c75aa59a4ec181fe2ec06cab30e198c1c6d107229a9f008ae3a7c16cabd8/ruff-0.16.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af1b576fddb9d9ef2ececfb5fadcd6a624b25070ed85e3cfcfe449fc3ff6a7b9", size = 10840857, upload-time = "2026-09-10T18:03:44.604Z" },
3803
- { url = "https://files.pythonhosted.org/packages/21/33/81f3da371942ea031105ba679d8d6e28ec1660ccd690a45f42d381161356/ruff-0.16.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce7f8f22df67c93ed96c717f9128eadb797144ac2bad475cf536f31d6100c55", size = 10370001, upload-time = "2026-09-10T18:03:46.706Z" },
3804
- { url = "https://files.pythonhosted.org/packages/fa/0b/6345fb4dbf6dd0ed1cfe5d18391dc9c3f59cc81622a7b0a65b84b3e730ba/ruff-0.16.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:06d0e93d04f392996435ebd600c153f65b47d73fbec2415aa99c5ee5756b3a5f", size = 10548735, upload-time = "2026-09-10T18:03:48.658Z" },
3805
- { url = "https://files.pythonhosted.org/packages/3f/4d/c5576adf511f92a328e5569dda190ecdd430da51f1a649f3a4a2fd73e21e/ruff-0.16.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:142151a5e7b93c1b11111337142f89dd2fbfee92161225c99a97222f22e32656", size = 10108496, upload-time = "2026-09-10T18:03:50.563Z" },
3806
- { url = "https://files.pythonhosted.org/packages/ff/8c/667d83c16199a17a56adc6b0bd4c3beb5b767a2babcd16a56f76f9be7fd6/ruff-0.16.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6651f97a342d8b35d54d8991544ca22169b86dc54111cb604666940c431b750", size = 9860136, upload-time = "2026-09-10T18:03:52.621Z" },
3807
- { url = "https://files.pythonhosted.org/packages/99/75/78d401106731999a1dd20cc5a6961e37e1eb9397a3b589f73f3a5ce146a3/ruff-0.16.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ef140c6eb935fa9a84c9c607dfb2cb1b85843c192e79265b0c54f35f557ea8e5", size = 10286290, upload-time = "2026-09-10T18:03:55.207Z" },
3808
- { url = "https://files.pythonhosted.org/packages/68/49/56f9c3a8b755df93a0ad318b2147bf4ef5dae9a7e5ec61c460109c67957f/ruff-0.16.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53e39506a730fadeee0d998ed5946f30671f0db240c6c7c73bdabbe33604bb6f", size = 10745048, upload-time = "2026-09-10T18:03:57.299Z" },
3809
- { url = "https://files.pythonhosted.org/packages/5f/ea/7f9b938a63ece4bec677ad7f9f7fa02df3383db1949ed93a382441c09a87/ruff-0.16.7-py3-none-win32.whl", hash = "sha256:2ea3470fcebcbc5df2fb0c6f3b90333fa9084c534e0111c038fa4a6ab9f1c4b7", size = 10059082, upload-time = "2026-09-10T18:03:59.632Z" },
3810
- { url = "https://files.pythonhosted.org/packages/39/11/480a6973a927aa653e1cead6a6416008640e03a99d05b34c0434b8c6c366/ruff-0.16.7-py3-none-win_amd64.whl", hash = "sha256:7ac26aca826e9e21d0f1cb25b54ac660760a9fdd094d3e4df9848232be98cfc6", size = 10593368, upload-time = "2026-09-10T18:04:01.999Z" },
3811
- { url = "https://files.pythonhosted.org/packages/8b/4b/51327018d056f0dad2c2238f26d1fb0f53707a9d91b75dea6d1b3039f136/ruff-0.16.7-py3-none-win_arm64.whl", hash = "sha256:aab7f39e2c9df6c596216070f98eef1207b94f8516cca20c808826974971855b", size = 10412401, upload-time = "2026-09-10T18:04:04.098Z" },
3791
+ version = "0.16.8"
3792
+ source = { registry = "https://pypi.org/simple" }
3793
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" }
3794
+ wheels = [
3795
+ { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" },
3796
+ { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" },
3797
+ { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" },
3798
+ { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" },
3799
+ { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" },
3800
+ { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" },
3801
+ { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" },
3802
+ { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" },
3803
+ { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" },
3804
+ { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" },
3805
+ { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" },
3806
+ { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" },
3807
+ { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" },
3808
+ { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" },
3809
+ { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" },
3810
+ { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" },
3811
+ { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" },
3812
3812
  ]
3813
3813
 
3814
3814
  [[package]]
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
File without changes