pyobs-gui 2.0.0.dev4__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 (147) hide show
  1. pyobs_gui-2.0.0.dev5/.gitignore +8 -0
  2. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/PKG-INFO +4 -2
  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.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/compassmovewidget.py +2 -0
  6. pyobs_gui-2.0.0.dev5/pyobs_gui/gui.py +173 -0
  7. pyobs_gui-2.0.0.dev5/pyobs_gui/login.py +64 -0
  8. pyobs_gui-2.0.0.dev5/pyobs_gui/loginwindow.py +377 -0
  9. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/logmodel.py +11 -0
  10. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/mainwindow.py +89 -22
  11. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/mainwindow.ui +195 -115
  12. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/mainwindow_ui.py +75 -20
  13. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/statuswidget.py +26 -2
  14. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/telescopewidget.py +19 -2
  15. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/utils.py +15 -2
  16. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/weatherwidget.py +6 -4
  17. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyproject.toml +12 -4
  18. pyobs_gui-2.0.0.dev5/specs/README.md +20 -0
  19. pyobs_gui-2.0.0.dev5/spike_standalone/main.py +84 -0
  20. pyobs_gui-2.0.0.dev5/spike_standalone/pysidedeploy.spec +97 -0
  21. pyobs_gui-2.0.0.dev5/tests/__init__.py +0 -0
  22. pyobs_gui-2.0.0.dev5/tests/conftest.py +20 -0
  23. pyobs_gui-2.0.0.dev5/tests/test_accounts.py +106 -0
  24. pyobs_gui-2.0.0.dev5/tests/test_loginwindow.py +196 -0
  25. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/uv.lock +275 -154
  26. pyobs_gui-2.0.0.dev4/pyobs_gui/gui.py +0 -92
  27. pyobs_gui-2.0.0.dev4/specs/README.md +0 -11
  28. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.claude/skills/verify/SKILL.md +0 -0
  29. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.github/dependabot.yml +0 -0
  30. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.github/workflows/pypi.yml +0 -0
  31. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.github/workflows/pyrefly.yml +0 -0
  32. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.github/workflows/ruff.yml +0 -0
  33. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.pre-commit-config.yaml +0 -0
  34. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.readthedocs.yml +0 -0
  35. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/CHANGELOG.rst +0 -0
  36. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEVELOPMENT.md +0 -0
  37. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_IAcquisition.md +0 -0
  38. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_IAutoFocus.md +0 -0
  39. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_IAutoGuiding.md +0 -0
  40. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_NavbarShortcuts.md +0 -0
  41. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_qml_evaluation.md +0 -0
  42. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_telescopewidget_layout.md +0 -0
  43. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/LICENSE +0 -0
  44. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/README.md +0 -0
  45. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/bin/mypyuic5 +0 -0
  46. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/Makefile +0 -0
  47. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/make.bat +0 -0
  48. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/requirements.txt +0 -0
  49. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/source/_static/pyobs.gif +0 -0
  50. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/source/conf.py +0 -0
  51. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/source/index.rst +0 -0
  52. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/mypy.ini +0 -0
  53. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/__init__.py +0 -0
  54. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/acquisitionwidget.py +0 -0
  55. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/autofocuswidget.py +0 -0
  56. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/autoguidingwidget.py +0 -0
  57. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/base.py +0 -0
  58. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/camerawidget.py +0 -0
  59. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/commandinputwidget.py +0 -0
  60. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/coolingwidget.py +0 -0
  61. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/datadisplaywidget.py +0 -0
  62. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/eventswidget.py +0 -0
  63. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/filterwidget.py +0 -0
  64. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/fitsheaderswidget.py +0 -0
  65. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/focuswidget.py +0 -0
  66. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modewidget.py +0 -0
  67. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modifiedmixin.py +0 -0
  68. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modulegui.py +0 -0
  69. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/__init__.py +0 -0
  70. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  71. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  72. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  73. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  74. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  75. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  76. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/camerawidget.ui +0 -0
  77. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  78. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  79. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget.py +0 -0
  80. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  81. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  82. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compile.sh +0 -0
  83. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/coolingwidget.ui +0 -0
  84. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  85. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  86. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  87. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  88. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/designer.sh +0 -0
  89. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/eventswidget.ui +0 -0
  90. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  91. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/filterwidget.ui +0 -0
  92. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  93. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  94. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  95. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/focuswidget.ui +0 -0
  96. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  97. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/modewidget.ui +0 -0
  98. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/modewidget_ui.py +0 -0
  99. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  100. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  101. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  102. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  103. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  104. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  105. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  106. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  107. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  108. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  109. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  110. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  111. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources.qrc +0 -0
  112. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources_rc.py +0 -0
  113. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/roofwidget.ui +0 -0
  114. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  115. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/shellwidget.ui +0 -0
  116. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  117. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  118. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  119. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/telescopewidget.ui +0 -0
  120. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  121. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  122. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  123. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  124. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  125. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/videowidget.ui +0 -0
  126. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/videowidget_ui.py +0 -0
  127. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/weatherwidget.ui +0 -0
  128. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  129. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/roofwidget.py +0 -0
  130. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/shellwidget.py +0 -0
  131. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/spectrographwidget.py +0 -0
  132. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/temperaturesplotwidget.py +0 -0
  133. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/temperatureswidget.py +0 -0
  134. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/videowidget.py +0 -0
  135. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/watchedlabel.py +0 -0
  136. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/acquisition.yaml +0 -0
  137. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/autofocus.yaml +0 -0
  138. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/camera.yaml +0 -0
  139. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/full.yaml +0 -0
  140. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/guiding.yaml +0 -0
  141. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/mode.yaml +0 -0
  142. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/roof.yaml +0 -0
  143. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/spectrograph.yaml +0 -0
  144. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/telescope.yaml +0 -0
  145. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/telescope_acl.yaml +0 -0
  146. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/telescope_acl_denied.yaml +0 -0
  147. {pyobs_gui-2.0.0.dev4 → 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.dev4
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
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"]
@@ -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 {}
@@ -0,0 +1,64 @@
1
+ """Async factory for pyobs.application.Application's module_factory contract -- see
2
+ specs/plans/gui-interactive-login.md and specs/plans/gui-login-window.md.
3
+
4
+ Shows LoginWindow, waits for Connect, then builds an XmppComm + GUI from the result. Runs inside
5
+ the already-running event loop (see Application._main()), so it can await anything it needs to
6
+ before the module/comm connection exists at all.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import logging
13
+ from typing import Any
14
+
15
+ from pyobs.comm.xmpp import XmppComm
16
+
17
+ from .gui import GUI
18
+ from .loginwindow import LoginWindow
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+
23
+ async def show_login_and_connect() -> XmppComm:
24
+ """Shows the login window, waits for the user to click Connect (or raises
25
+ asyncio.CancelledError if they close the window instead -- see LoginWindow.closeEvent), then
26
+ returns a fresh XmppComm built from the result.
27
+
28
+ Used both for the initial standalone startup (login_and_build_gui) and for GUI's "Log out"
29
+ flow (GUI._logout), which shows this same window again without tearing down the running
30
+ Application/event loop.
31
+ """
32
+ window = LoginWindow()
33
+ window.show()
34
+ try:
35
+ request = await window.wait_for_connect()
36
+ except asyncio.CancelledError:
37
+ log.info("Login window closed without connecting.")
38
+ raise
39
+ finally:
40
+ window.close()
41
+
42
+ log.info("Connecting as %s...", request.jid)
43
+ return XmppComm(
44
+ jid=request.jid,
45
+ password=request.password,
46
+ server=request.server,
47
+ use_tls=request.use_tls,
48
+ ignore_cert_errors=request.insecure_skip_tls,
49
+ )
50
+
51
+
52
+ async def login_and_build_gui(**gui_kwargs: Any) -> GUI:
53
+ """Shows the login window, waits for the user to click Connect (or cancels if they close the
54
+ window instead -- see LoginWindow.closeEvent), then returns a GUI built from the result.
55
+
56
+ Args:
57
+ gui_kwargs: Passed through to GUI(...) (show_shell, show_events, widgets, etc.) --
58
+ everything except `comm`, which is built here from the login window's result.
59
+ """
60
+ comm = await show_login_and_connect()
61
+ return GUI(comm=comm, standalone=True, **gui_kwargs)
62
+
63
+
64
+ __all__ = ["login_and_build_gui", "show_login_and_connect"]