pyobs-gui 2.0.0.dev3__tar.gz → 2.0.0.dev5__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 (146) hide show
  1. pyobs_gui-2.0.0.dev5/.gitignore +8 -0
  2. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/PKG-INFO +5 -3
  3. pyobs_gui-2.0.0.dev5/pyobs_gui/__main__.py +21 -0
  4. pyobs_gui-2.0.0.dev5/pyobs_gui/accounts.py +150 -0
  5. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/camerawidget.py +121 -41
  6. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/compassmovewidget.py +2 -0
  7. pyobs_gui-2.0.0.dev5/pyobs_gui/gui.py +173 -0
  8. pyobs_gui-2.0.0.dev5/pyobs_gui/login.py +64 -0
  9. pyobs_gui-2.0.0.dev5/pyobs_gui/loginwindow.py +377 -0
  10. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/logmodel.py +11 -0
  11. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/mainwindow.py +89 -22
  12. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/camerawidget.ui +28 -43
  13. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/camerawidget_ui.py +61 -128
  14. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/mainwindow.ui +195 -115
  15. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/mainwindow_ui.py +75 -20
  16. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/statuswidget.py +26 -2
  17. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/telescopewidget.py +19 -2
  18. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/utils.py +15 -2
  19. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/weatherwidget.py +6 -4
  20. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyproject.toml +13 -6
  21. pyobs_gui-2.0.0.dev5/specs/README.md +20 -0
  22. pyobs_gui-2.0.0.dev5/spike_standalone/main.py +84 -0
  23. pyobs_gui-2.0.0.dev5/spike_standalone/pysidedeploy.spec +97 -0
  24. pyobs_gui-2.0.0.dev5/tests/__init__.py +0 -0
  25. pyobs_gui-2.0.0.dev5/tests/conftest.py +20 -0
  26. pyobs_gui-2.0.0.dev5/tests/test_accounts.py +106 -0
  27. pyobs_gui-2.0.0.dev5/tests/test_loginwindow.py +196 -0
  28. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/uv.lock +369 -269
  29. pyobs_gui-2.0.0.dev3/pyobs_gui/gui.py +0 -92
  30. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.claude/skills/verify/SKILL.md +0 -0
  31. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.github/dependabot.yml +0 -0
  32. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.github/workflows/pypi.yml +0 -0
  33. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.github/workflows/pyrefly.yml +0 -0
  34. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.github/workflows/ruff.yml +0 -0
  35. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.pre-commit-config.yaml +0 -0
  36. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/.readthedocs.yml +0 -0
  37. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/CHANGELOG.rst +0 -0
  38. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEVELOPMENT.md +0 -0
  39. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_IAcquisition.md +0 -0
  40. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_IAutoFocus.md +0 -0
  41. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_IAutoGuiding.md +0 -0
  42. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_NavbarShortcuts.md +0 -0
  43. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_qml_evaluation.md +0 -0
  44. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/DEV_telescopewidget_layout.md +0 -0
  45. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/LICENSE +0 -0
  46. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/README.md +0 -0
  47. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/bin/mypyuic5 +0 -0
  48. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/Makefile +0 -0
  49. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/make.bat +0 -0
  50. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/requirements.txt +0 -0
  51. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/source/_static/pyobs.gif +0 -0
  52. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/source/conf.py +0 -0
  53. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/docs/source/index.rst +0 -0
  54. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/mypy.ini +0 -0
  55. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/__init__.py +0 -0
  56. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/acquisitionwidget.py +0 -0
  57. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/autofocuswidget.py +0 -0
  58. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/autoguidingwidget.py +0 -0
  59. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/base.py +0 -0
  60. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/commandinputwidget.py +0 -0
  61. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/coolingwidget.py +0 -0
  62. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/datadisplaywidget.py +0 -0
  63. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/eventswidget.py +0 -0
  64. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/filterwidget.py +0 -0
  65. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/fitsheaderswidget.py +0 -0
  66. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/focuswidget.py +0 -0
  67. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modewidget.py +0 -0
  68. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modifiedmixin.py +0 -0
  69. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modulegui.py +0 -0
  70. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/__init__.py +0 -0
  71. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  72. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  73. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  74. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  75. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  76. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  77. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  78. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget.py +0 -0
  79. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  80. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  81. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compile.sh +0 -0
  82. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/coolingwidget.ui +0 -0
  83. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  84. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  85. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  86. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  87. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/designer.sh +0 -0
  88. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/eventswidget.ui +0 -0
  89. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  90. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/filterwidget.ui +0 -0
  91. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  92. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  93. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  94. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/focuswidget.ui +0 -0
  95. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  96. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/modewidget.ui +0 -0
  97. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/modewidget_ui.py +0 -0
  98. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  99. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  100. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  101. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  102. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  103. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  104. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  105. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  106. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  107. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  108. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  109. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  110. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources.qrc +0 -0
  111. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources_rc.py +0 -0
  112. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/roofwidget.ui +0 -0
  113. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  114. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/shellwidget.ui +0 -0
  115. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  116. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  117. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  118. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/telescopewidget.ui +0 -0
  119. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  120. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  121. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  122. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  123. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  124. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/videowidget.ui +0 -0
  125. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/videowidget_ui.py +0 -0
  126. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/weatherwidget.ui +0 -0
  127. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  128. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/roofwidget.py +0 -0
  129. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/shellwidget.py +0 -0
  130. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/spectrographwidget.py +0 -0
  131. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/temperaturesplotwidget.py +0 -0
  132. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/temperatureswidget.py +0 -0
  133. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/videowidget.py +0 -0
  134. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/pyobs_gui/watchedlabel.py +0 -0
  135. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/acquisition.yaml +0 -0
  136. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/autofocus.yaml +0 -0
  137. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/camera.yaml +0 -0
  138. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/full.yaml +0 -0
  139. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/guiding.yaml +0 -0
  140. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/mode.yaml +0 -0
  141. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/roof.yaml +0 -0
  142. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/spectrograph.yaml +0 -0
  143. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/telescope.yaml +0 -0
  144. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/telescope_acl.yaml +0 -0
  145. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/telescope_acl_denied.yaml +0 -0
  146. {pyobs_gui-2.0.0.dev3 → pyobs_gui-2.0.0.dev5}/test/video.yaml +0 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+
4
+ # pyside6-deploy build output for the spikes in spike_standalone/ -- generated, not source
5
+ spike_standalone/deployment/
6
+ spike_standalone/*.dist/
7
+ spike_standalone/build*.log
8
+ spike_standalone/spike_results.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyobs-gui
3
- Version: 2.0.0.dev3
3
+ Version: 2.0.0.dev5
4
4
  Summary: A remote GUI for pyobs
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -9,10 +9,12 @@ Requires-Python: <3.14,>=3.11
9
9
  Requires-Dist: astroplan>=0.10.1
10
10
  Requires-Dist: astropy<8,>=7.0.1
11
11
  Requires-Dist: colour<0.2,>=0.1.5
12
+ Requires-Dist: keyring<26,>=25.7.0
12
13
  Requires-Dist: matplotlib<4,>=3.10.1
13
- Requires-Dist: pyobs-core<3,>=2.0.0.dev28
14
+ Requires-Dist: pyobs-core<3,>=2.0.0.dev50
14
15
  Requires-Dist: pyside6>=6.10.1
15
- Requires-Dist: qasync<0.28,>=0.27.1
16
+ Requires-Dist: qasync<0.29,>=0.27.1
16
17
  Requires-Dist: qfitswidget>=0.13.1
17
18
  Requires-Dist: qtawesome<2,>=1.4.0
19
+ Requires-Dist: secretstorage<4,>=3.5.0
18
20
  Requires-Dist: sunpy[all]>=7.0.1
@@ -0,0 +1,21 @@
1
+ """Standalone entry point for an interactive-login pyobs-gui -- bypasses the config-file-driven
2
+ `pyobs` CLI entirely. This is what a compiled binary (see
3
+ specs/plans/gui-widget-plugins-and-packaging.md) targets: no YAML config, no credentials on disk
4
+ unless the user opts into "store password" in the login window.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pyobs.application import Application
10
+
11
+ from .gui import GUI
12
+ from .login import login_and_build_gui
13
+
14
+
15
+ def main() -> None:
16
+ app = Application(module_factory=login_and_build_gui, loop_module_class=GUI)
17
+ app.run()
18
+
19
+
20
+ if __name__ == "__main__":
21
+ main()
@@ -0,0 +1,150 @@
1
+ """Saved XMPP login accounts, persisted via QSettings -- see specs/plans/gui-login-window.md.
2
+
3
+ Each account has a stable internal id (a uuid4 hex string, generated in add_account()) distinct
4
+ from its jid/label -- both are freely editable via update_account(), and the id (not jid/label) is
5
+ what a stored password gets keyed on in the system keychain, so renaming an account never orphans
6
+ its password. Mirrors pyobs-polaris's SavedAccountsModel, which follows the same rule for the same
7
+ reason.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import uuid
13
+ from dataclasses import dataclass
14
+
15
+ from PySide6 import QtCore
16
+
17
+ _ORGANIZATION = "pyobs"
18
+ _APPLICATION = "pyobs-gui"
19
+
20
+
21
+ @dataclass
22
+ class Account:
23
+ id: str
24
+ jid: str
25
+ label: str = ""
26
+ host: str = ""
27
+ port: int = 0
28
+ use_tls: bool = True
29
+ insecure_skip_tls: bool = False
30
+
31
+ @property
32
+ def display_name(self) -> str:
33
+ return self.label if self.label else self.jid
34
+
35
+
36
+ class SavedAccountsModel:
37
+ """Saved account list, persisted via QSettings. Password storage is a separate concern --
38
+ see login.py's use of the `keyring` package, keyed by Account.id."""
39
+
40
+ def __init__(self, settings: QtCore.QSettings | None = None) -> None:
41
+ self._settings = settings if settings is not None else QtCore.QSettings(_ORGANIZATION, _APPLICATION)
42
+
43
+ def list_accounts(self) -> list[Account]:
44
+ accounts = []
45
+ size = self._settings.beginReadArray("accounts")
46
+ try:
47
+ for i in range(size):
48
+ self._settings.setArrayIndex(i)
49
+ accounts.append(
50
+ Account(
51
+ id=str(self._settings.value("id", "")),
52
+ jid=str(self._settings.value("jid", "")),
53
+ label=str(self._settings.value("label", "")),
54
+ host=str(self._settings.value("host", "")),
55
+ port=int(self._settings.value("port", 0, type=int)), # pyrefly: ignore [bad-argument-type]
56
+ use_tls=bool(self._settings.value("use_tls", True, type=bool)),
57
+ insecure_skip_tls=bool(self._settings.value("insecure_skip_tls", False, type=bool)),
58
+ )
59
+ )
60
+ finally:
61
+ self._settings.endArray()
62
+ return accounts
63
+
64
+ def account_by_id(self, account_id: str) -> Account | None:
65
+ for account in self.list_accounts():
66
+ if account.id == account_id:
67
+ return account
68
+ return None
69
+
70
+ def add_account(
71
+ self,
72
+ jid: str,
73
+ label: str = "",
74
+ host: str = "",
75
+ port: int = 0,
76
+ use_tls: bool = True,
77
+ insecure_skip_tls: bool = False,
78
+ ) -> str:
79
+ """Adds a new account and returns its newly generated id."""
80
+ account = Account(
81
+ id=uuid.uuid4().hex,
82
+ jid=jid,
83
+ label=label,
84
+ host=host,
85
+ port=port,
86
+ use_tls=use_tls,
87
+ insecure_skip_tls=insecure_skip_tls,
88
+ )
89
+ accounts = self.list_accounts()
90
+ accounts.append(account)
91
+ self._write_all(accounts)
92
+ return account.id
93
+
94
+ def update_account(
95
+ self,
96
+ account_id: str,
97
+ jid: str,
98
+ label: str = "",
99
+ host: str = "",
100
+ port: int = 0,
101
+ use_tls: bool = True,
102
+ insecure_skip_tls: bool = False,
103
+ ) -> None:
104
+ """Updates an existing account's fields in place -- the id itself never changes."""
105
+ accounts = self.list_accounts()
106
+ for i, account in enumerate(accounts):
107
+ if account.id == account_id:
108
+ accounts[i] = Account(
109
+ id=account_id,
110
+ jid=jid,
111
+ label=label,
112
+ host=host,
113
+ port=port,
114
+ use_tls=use_tls,
115
+ insecure_skip_tls=insecure_skip_tls,
116
+ )
117
+ break
118
+ self._write_all(accounts)
119
+
120
+ def remove_account(self, account_id: str) -> None:
121
+ accounts = [a for a in self.list_accounts() if a.id != account_id]
122
+ self._write_all(accounts)
123
+
124
+ def _write_all(self, accounts: list[Account]) -> None:
125
+ self._settings.beginWriteArray("accounts")
126
+ try:
127
+ for i, account in enumerate(accounts):
128
+ self._settings.setArrayIndex(i)
129
+ self._settings.setValue("id", account.id)
130
+ self._settings.setValue("jid", account.jid)
131
+ self._settings.setValue("label", account.label)
132
+ self._settings.setValue("host", account.host)
133
+ self._settings.setValue("port", account.port)
134
+ self._settings.setValue("use_tls", account.use_tls)
135
+ self._settings.setValue("insecure_skip_tls", account.insecure_skip_tls)
136
+ finally:
137
+ self._settings.endArray()
138
+
139
+ @property
140
+ def last_selected_account_id(self) -> str:
141
+ """Only remembers which account was last used (to preselect it next launch) -- never
142
+ implies anything was saved/connected automatically."""
143
+ return str(self._settings.value("last_selected_account_id", ""))
144
+
145
+ @last_selected_account_id.setter
146
+ def last_selected_account_id(self, value: str) -> None:
147
+ self._settings.setValue("last_selected_account_id", value)
148
+
149
+
150
+ __all__ = ["Account", "SavedAccountsModel"]
@@ -17,7 +17,6 @@ from pyobs.interfaces import (
17
17
  IBinning,
18
18
  BinningState,
19
19
  IWindow,
20
- WindowState,
21
20
  IFilters,
22
21
  ICooling,
23
22
  ITemperatures,
@@ -58,13 +57,6 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
58
57
  self.exposure_time_left = 0.0
59
58
  self.exposure_progress = 0.0
60
59
 
61
- self.spinWindowLeft.init_modified(self.labelWindowLeft).committed.connect(self._window_changed)
62
- self.spinWindowTop.init_modified(self.labelWindowTop).committed.connect(self._window_changed)
63
- self.spinWindowWidth.init_modified(self.labelWindowWidth).committed.connect(self._window_changed)
64
- self.spinWindowHeight.init_modified(self.labelWindowHeight).committed.connect(self._window_changed)
65
- self.spinGain.init_modified(self.labelGain).committed.connect(self._gain_changed)
66
- self.spinGainOffset.init_modified(self.labelGain).committed.connect(self._gain_changed)
67
-
68
60
  async def open(
69
61
  self,
70
62
  modules: list[str] | None = None,
@@ -80,7 +72,7 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
80
72
  image_types = sorted([it.name for it in ImageType])
81
73
  self.comboImageType.addItems(image_types)
82
74
 
83
- # before first update, disable mys
75
+ # before first update, disable myself
84
76
  self.setEnabled(False)
85
77
 
86
78
  # hide groups, if necessary
@@ -118,10 +110,25 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
118
110
 
119
111
  async def _init(self) -> None:
120
112
  # window
113
+ window_caps = await self.comm.get_capabilities(self.module, IWindow)
114
+ if window_caps is not None:
115
+ self.spinWindowLeft.setMaximum(int(window_caps.full_frame_width))
116
+ self.spinWindowTop.setMaximum(int(window_caps.full_frame_height))
117
+ self.spinWindowWidth.setMaximum(int(window_caps.full_frame_width))
118
+ self.spinWindowHeight.setMaximum(int(window_caps.full_frame_height))
121
119
  async with self.comm.safe_proxy(self.module, IWindow) as proxy:
122
120
  if proxy is not None:
123
- await self.comm.subscribe_state(self.module, IWindow, self._update_window)
124
- await self.set_full_frame()
121
+ state = await proxy.wait_for_state(IWindow)
122
+ if state is not None:
123
+ self.spinWindowLeft.setValue(state.x)
124
+ self.spinWindowTop.setValue(state.y)
125
+ self.spinWindowWidth.setValue(state.width)
126
+ self.spinWindowHeight.setValue(state.height)
127
+ elif window_caps is not None:
128
+ self.spinWindowLeft.setValue(window_caps.full_frame_x)
129
+ self.spinWindowTop.setValue(window_caps.full_frame_y)
130
+ self.spinWindowWidth.setValue(window_caps.full_frame_width)
131
+ self.spinWindowHeight.setValue(window_caps.full_frame_height)
125
132
 
126
133
  # binning
127
134
  binning_caps = await self.comm.get_capabilities(self.module, IBinning)
@@ -129,11 +136,21 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
129
136
  binnings = [f"{b.x}x{b.y}" for b in binning_caps.binnings]
130
137
  self.comboBinning.clear()
131
138
  self.comboBinning.addItems(binnings)
132
- await self.comm.subscribe_state(self.module, IBinning, self._update_binning)
139
+ async with self.comm.safe_proxy(self.module, IBinning) as proxy:
140
+ if proxy is not None:
141
+ state = await proxy.wait_for_state(IBinning)
142
+ if state is not None:
143
+ self.comboBinning.setCurrentText(f"{state.x}x{state.y}")
144
+ await self.comm.subscribe_state(self.module, IBinning, self._update_binning)
133
145
 
134
- # gain — initial values delivered by subscription callback
135
- if await self.comm.has_proxy(self.module, IGain):
136
- await self.comm.subscribe_state(self.module, IGain, self._update_gain)
146
+ # gain
147
+ async with self.comm.safe_proxy(self.module, IGain) as proxy:
148
+ if proxy is not None:
149
+ state = await proxy.wait_for_state(IGain)
150
+ if state is not None:
151
+ self.spinGain.setValue(state.gain)
152
+ self.spinGainOffset.setValue(state.offset)
153
+ await self.comm.subscribe_state(self.module, IGain, self._update_gain)
137
154
 
138
155
  # image format
139
156
  image_format_caps = await self.comm.get_capabilities(self.module, IImageFormat)
@@ -141,38 +158,52 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
141
158
  image_formats = [ImageFormat(f) for f in image_format_caps.image_formats]
142
159
  self.comboImageFormat.clear()
143
160
  self.comboImageFormat.addItems([f.name for f in image_formats])
144
- await self.comm.subscribe_state(self.module, IImageFormat, self._update_image_format)
161
+ async with self.comm.safe_proxy(self.module, IImageFormat) as proxy:
162
+ if proxy is not None:
163
+ state = await proxy.wait_for_state(IImageFormat)
164
+ if state is not None:
165
+ self.comboImageFormat.setCurrentText(state.image_format.name)
166
+ await self.comm.subscribe_state(self.module, IImageFormat, self._update_image_format)
145
167
 
168
+ # image type
146
169
  async with self.comm.safe_proxy(self.module, IImageType) as proxy:
147
170
  if proxy is not None:
148
- await self.comm.subscribe_state(self.module, IImageType, self._update_image_type)
171
+ state = await proxy.wait_for_state(IImageType)
172
+ if state is not None:
173
+ self.comboImageType.setCurrentText(state.image_type.name)
174
+ await self.comm.subscribe_state(self.module, IImageType, self._update_image_type)
149
175
 
176
+ # exposure (status, progress, time left)
150
177
  async with self.comm.safe_proxy(self.module, IExposure) as proxy:
151
178
  if proxy is not None:
152
- await self.comm.subscribe_state(self.module, IExposure, self._update_exposure)
153
-
179
+ state = await proxy.wait_for_state(IExposure)
180
+ if state is not None:
181
+ self.exposure_status = state.status
182
+ self.exposure_progress = state.progress
183
+ self.exposure_time_left = state.exposure_time_left
184
+ await self.comm.subscribe_state(self.module, IExposure, self._update_exposure)
185
+
186
+ # exposure time
154
187
  async with self.comm.safe_proxy(self.module, IExposureTime) as proxy:
155
188
  if proxy is not None:
156
- await self.comm.subscribe_state(self.module, IExposureTime, self._update_exposure_time)
189
+ state = await proxy.wait_for_state(IExposureTime)
190
+ if state is not None:
191
+ self.spinExpTime.setValue(state.exposure_time)
192
+ await self.comm.subscribe_state(self.module, IExposureTime, self._update_exposure_time)
157
193
 
194
+ # data sequence
158
195
  if await self.comm.has_proxy(self.module, IDataSequence):
159
196
  await self.comm.subscribe_state(self.module, IDataSequence, self._update_sequence)
160
197
 
161
198
  # update GUI
162
199
  self.signal_update_gui.emit()
163
200
 
164
- def _update_window(self, state: WindowState):
165
- self.labelWindowLeft.setText(str(state.x))
166
- self.labelWindowTop.setText(str(state.y))
167
- self.labelWindowWidth.setText(str(state.width))
168
- self.labelWindowHeight.setText(str(state.height))
169
-
170
201
  def _update_binning(self, state: BinningState):
171
202
  self.comboBinning.setCurrentText(f"{state.x}x{state.y}")
172
203
 
173
204
  def _update_gain(self, state: GainState):
174
- self.labelGain.setText(str(state.gain))
175
- self.labelGainOffset.setText(str(state.offset))
205
+ self.spinGain.setValue(state.gain)
206
+ self.spinGainOffset.setValue(state.offset)
176
207
 
177
208
  def _update_image_format(self, state: ImageFormatState):
178
209
  self.comboImageFormat.setCurrentText(state.image_format.name)
@@ -194,14 +225,6 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
194
225
  self.exposures_left = state.count_left
195
226
  self.update_gui()
196
227
 
197
- @qasync.asyncSlot()
198
- async def _window_changed(self):
199
- print("ok")
200
-
201
- @qasync.asyncSlot()
202
- async def _gain_changed(self):
203
- print("ok")
204
-
205
228
  @qasync.asyncSlot() # type: ignore
206
229
  async def set_full_frame(self) -> None:
207
230
  caps = await self.comm.get_capabilities(self.module, IWindow)
@@ -247,18 +270,75 @@ class CameraWidget(BaseWidget, Ui_CameraWidget):
247
270
 
248
271
  @qasync.asyncSlot() # type: ignore
249
272
  async def expose(self) -> None:
273
+ # set binning
274
+ async with self.comm.safe_proxy(self.module, IBinning) as proxy:
275
+ if proxy is not None:
276
+ binning = int(self.comboBinning.currentText()[0])
277
+ try:
278
+ await proxy.set_binning(binning, binning)
279
+ except Exception:
280
+ log.exception("bla")
281
+ QtWidgets.QMessageBox.information(self, "Error", "Could not set binning.")
282
+ return
283
+ else:
284
+ binning = 1
285
+
286
+ # set window
287
+ async with self.comm.safe_proxy(self.module, IWindow) as proxy:
288
+ if proxy is not None:
289
+ left, top = self.spinWindowLeft.value(), self.spinWindowTop.value()
290
+ width, height = self.spinWindowWidth.value(), self.spinWindowHeight.value()
291
+ try:
292
+ await proxy.set_window(left, top, width * binning, height * binning)
293
+ except Exception:
294
+ QtWidgets.QMessageBox.information(self, "Error", "Could not set window.")
295
+ return
296
+
297
+ # set image format
298
+ async with self.comm.safe_proxy(self.module, IImageFormat) as proxy:
299
+ if proxy is not None:
300
+ image_format = ImageFormat[self.comboImageFormat.currentText()]
301
+ await proxy.set_image_format(image_format)
302
+
303
+ # set exposure time
304
+ async with self.comm.safe_proxy(self.module, IExposureTime) as proxy:
305
+ if proxy is not None:
306
+ # get exp_time
307
+ exp_time = self.spinExpTime.value()
308
+
309
+ # unit
310
+ if self.comboExpTimeUnit.currentText() == "ms":
311
+ exp_time /= 1e3
312
+ elif self.comboExpTimeUnit.currentText() == "µs":
313
+ exp_time /= 1e6
314
+
315
+ # set it
316
+ await proxy.set_exposure_time(exp_time)
317
+
318
+ # set gain and offset
319
+ async with self.comm.safe_proxy(self.module, IGain) as proxy:
320
+ if proxy is not None:
321
+ await proxy.set_gain(self.spinGain.value())
322
+ await proxy.set_offset(self.spinGainOffset.value())
323
+
324
+ # set image type
325
+ image_type = ImageType.OBJECT
326
+ async with self.comm.safe_proxy(self.module, IImageType) as proxy:
327
+ if proxy is not None:
328
+ image_type = ImageType(self.comboImageType.currentText().lower())
329
+ await proxy.set_image_type(image_type)
330
+
331
+ # set initial image count
332
+ self.exposures_left = self.spinCount.value()
250
333
  broadcast = self.checkBroadcast.isChecked()
251
- count = self.spinCount.value()
252
334
 
253
- # if the module can grab a counted sequence server-side, let it -- progress comes
254
- # back via the DataSequenceState subscription
335
+ # if the module can grab a counted sequence server-side, let it
255
336
  async with self.comm.safe_proxy(self.module, IDataSequence) as proxy:
256
337
  if proxy is not None:
257
- await proxy.grab_sequence(count, broadcast)
338
+ await proxy.grab_sequence(self.exposures_left, broadcast)
258
339
  return
259
340
 
260
341
  # fall back to a client-side loop for modules that don't support IDataSequence
261
- self.exposures_left = count
262
342
  while self.exposures_left > 0:
263
343
  await self.datadisplay.grab_data(broadcast)
264
344
  self.exposures_left -= 1
@@ -46,7 +46,9 @@ class CompassMoveWidget(BaseWidget, Ui_CompassMoveWidget):
46
46
  if altaz_state is None:
47
47
  return
48
48
  altaz = SkyCoord(
49
+ # pyrefly: ignore [missing-attribute]
49
50
  alt=altaz_state.alt * u.degree,
51
+ # pyrefly: ignore [missing-attribute]
50
52
  az=altaz_state.az * u.degree,
51
53
  obstime=Time.now(),
52
54
  location=self.observer.location,
@@ -0,0 +1,173 @@
1
+ import asyncio
2
+ import logging
3
+ import sys
4
+ from typing import Any, cast, TYPE_CHECKING
5
+ from PySide6 import QtWidgets # type: ignore
6
+ import qasync # type: ignore
7
+
8
+ if TYPE_CHECKING:
9
+ from .mainwindow import MainWindow
10
+
11
+ from pyobs.interfaces import FitsHeaderEntry, IFitsHeaderBefore
12
+ from pyobs.modules import Module
13
+
14
+ from .utils import QAsyncMessageBox
15
+
16
+ log = logging.getLogger(__name__)
17
+
18
+
19
+ class GUI(Module, IFitsHeaderBefore):
20
+ __module__ = "pyobs_gui"
21
+
22
+ app: QtWidgets.QApplication | None = None
23
+
24
+ def __init__(
25
+ self,
26
+ show_shell: bool = True,
27
+ show_events: bool = True,
28
+ show_status: bool = True,
29
+ show_modules: list[str] | None = None,
30
+ widgets: list[dict[str, Any]] | None = None,
31
+ sidebar: list[dict[str, Any]] | None = None,
32
+ standalone: bool = False,
33
+ *args: Any,
34
+ **kwargs: Any,
35
+ ):
36
+ """Inits a new GUI.
37
+
38
+ Args:
39
+ show_shell: Whether to show the shell page.
40
+ show_events: Whether to show the events page.
41
+ show_modules: If not empty, show only listed modules.
42
+ widgets: List of custom widgets.
43
+ sidebar: List of custom sidebar widgets.
44
+ standalone: Whether this GUI was built by the interactive login window
45
+ (pyobs_gui.login.login_and_build_gui) rather than a YAML-configured Comm. When
46
+ True, MainWindow's bottom-left button reads "Log out" and reconnects via the
47
+ login window instead of quitting the whole app.
48
+ """
49
+
50
+ # init module
51
+ Module.__init__(self, *args, **kwargs)
52
+ self._window: MainWindow | None = None
53
+ self._show_shell = show_shell
54
+ self._show_events = show_events
55
+ self._show_status = show_status
56
+ self._show_modules = show_modules
57
+ self._custom_widgets = widgets
58
+ self._custom_sidebar_widgets = sidebar
59
+ self._standalone = standalone
60
+ self._logging_out = False
61
+
62
+ @staticmethod
63
+ def new_event_loop() -> asyncio.AbstractEventLoop:
64
+ GUI.app = QtWidgets.QApplication(sys.argv)
65
+ # standalone/login mode briefly has zero visible top-level windows while the login
66
+ # dialog closes and the (old or new) MainWindow hasn't opened yet -- Qt's default
67
+ # "quit when the last window closes" would tear down the whole event loop right there,
68
+ # so this app manages its own lifetime exclusively through Module.quit() instead.
69
+ GUI.app.setQuitOnLastWindowClosed(False)
70
+ return cast("asyncio.AbstractEventLoop", qasync.QEventLoop(GUI.app))
71
+
72
+ async def open(self) -> None:
73
+ """Open module."""
74
+ from .mainwindow import MainWindow
75
+
76
+ await Module.open(self)
77
+
78
+ # create and show window
79
+ self._window = MainWindow(
80
+ show_shell=self._show_shell,
81
+ show_events=self._show_events,
82
+ show_status=self._show_status,
83
+ show_modules=self._show_modules,
84
+ widgets=self._custom_widgets,
85
+ sidebar=self._custom_sidebar_widgets,
86
+ on_logout=self._request_logout if self._standalone else None,
87
+ )
88
+ await self._window.open(
89
+ module=self,
90
+ comm=self.comm,
91
+ vfs=self.vfs,
92
+ observer=self._observer,
93
+ )
94
+ self._window.show()
95
+
96
+ def _request_logout(self) -> None:
97
+ """Wired to MainWindow's "Log out" button (standalone mode only). Hides the current
98
+ window immediately -- like a real logout, not a "maybe" -- then schedules the async part
99
+ (unregistering its widgets' comm subscriptions, closing the comm, showing the login
100
+ window again) as a background task, since Qt click handlers are synchronous.
101
+ """
102
+ if self._logging_out:
103
+ return # already logging out -- ignore a repeat click
104
+ self._logging_out = True
105
+
106
+ old_window = self._window
107
+ self._window = None
108
+ if old_window is not None:
109
+ old_window.close_for_logout()
110
+
111
+ asyncio.create_task(self._logout(old_window))
112
+
113
+ async def _logout(self, old_window: "MainWindow | None") -> None:
114
+ """Closes the current comm, then shows the login window and reconnects with whatever it
115
+ resolves to -- all within the same running Application/event loop, so Ctrl+C and the
116
+ eventual real "Quit" still reach this exact module instance (see
117
+ specs/plans/gui-login-window.md for why a brand new GUI() instance would leave
118
+ Application's own bookkeeping stale instead).
119
+
120
+ By the time this runs, MainWindow._request_logout() has already hidden the old window --
121
+ there's no session left to fall back to, so any failure here (the user closes the login
122
+ window without connecting, or the reconnect itself fails) is reported via an error dialog
123
+ and the app then quits cleanly, rather than limping on with no window and no comm.
124
+ """
125
+ if old_window is not None:
126
+ # must happen before deleteLater(): unregisters every widget's comm subscriptions
127
+ # first, so a stray in-flight callback can't fire against an already-destroyed label
128
+ await old_window.discard_all_widgets()
129
+ old_window.deleteLater()
130
+
131
+ old_comm = self._comm
132
+ if old_comm is not None:
133
+ await old_comm.close()
134
+
135
+ from .login import show_login_and_connect
136
+
137
+ try:
138
+ new_comm = await show_login_and_connect()
139
+ except asyncio.CancelledError:
140
+ log.info("Login window closed without connecting; nothing left to do but quit.")
141
+ self.quit()
142
+ return
143
+ except Exception as e:
144
+ log.exception("Could not build a new connection.")
145
+ await QAsyncMessageBox.critical(None, "Log out", f"Could not reconnect: {e}\n\nClosing the app.")
146
+ self.quit()
147
+ return
148
+ finally:
149
+ self._logging_out = False
150
+
151
+ self._comm = new_comm
152
+ try:
153
+ await self.startup()
154
+ except Exception as e:
155
+ log.exception("Reconnecting with the new account failed.")
156
+ await QAsyncMessageBox.critical(None, "Log out", f"Could not reconnect: {e}\n\nClosing the app.")
157
+ self.quit()
158
+
159
+ async def get_fits_header_before(
160
+ self, namespaces: list[str] | None = None, **kwargs: Any
161
+ ) -> dict[str, FitsHeaderEntry]:
162
+ """Returns FITS header for the current status of this module.
163
+
164
+ Args:
165
+ namespaces: If given, only return FITS headers for the given namespaces.
166
+
167
+ Returns:
168
+ Dictionary containing FITS headers.
169
+ """
170
+ if self._window is not None:
171
+ return self._window.get_fits_headers(namespaces)
172
+ else:
173
+ return {}