pyobs-gui 2.0.0.dev4__tar.gz → 2.0.0.dev6__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 (149) hide show
  1. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/.claude/skills/verify/SKILL.md +1 -1
  2. pyobs_gui-2.0.0.dev6/.gitignore +13 -0
  3. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/PKG-INFO +4 -2
  4. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/__init__.py +4 -0
  5. pyobs_gui-2.0.0.dev6/pyobs_gui/__main__.py +21 -0
  6. pyobs_gui-2.0.0.dev6/pyobs_gui/_nuitka_astropy_patch.py +453 -0
  7. pyobs_gui-2.0.0.dev6/pyobs_gui/accounts.py +150 -0
  8. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/compassmovewidget.py +2 -0
  9. pyobs_gui-2.0.0.dev6/pyobs_gui/gui.py +173 -0
  10. pyobs_gui-2.0.0.dev6/pyobs_gui/login.py +64 -0
  11. pyobs_gui-2.0.0.dev6/pyobs_gui/loginwindow.py +377 -0
  12. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/logmodel.py +11 -0
  13. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/mainwindow.py +92 -25
  14. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/mainwindow.ui +195 -115
  15. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/mainwindow_ui.py +75 -20
  16. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/statuswidget.py +26 -2
  17. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/telescopewidget.py +19 -2
  18. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/utils.py +15 -2
  19. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/weatherwidget.py +6 -4
  20. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyproject.toml +15 -4
  21. pyobs_gui-2.0.0.dev6/pysidedeploy.spec +97 -0
  22. pyobs_gui-2.0.0.dev6/specs/README.md +34 -0
  23. pyobs_gui-2.0.0.dev6/spike_standalone/main.py +84 -0
  24. pyobs_gui-2.0.0.dev6/spike_standalone/pysidedeploy.spec +97 -0
  25. pyobs_gui-2.0.0.dev6/tests/__init__.py +0 -0
  26. pyobs_gui-2.0.0.dev6/tests/conftest.py +20 -0
  27. pyobs_gui-2.0.0.dev6/tests/test_accounts.py +106 -0
  28. pyobs_gui-2.0.0.dev6/tests/test_loginwindow.py +196 -0
  29. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/uv.lock +275 -154
  30. pyobs_gui-2.0.0.dev4/DEVELOPMENT.md +0 -155
  31. pyobs_gui-2.0.0.dev4/DEV_IAcquisition.md +0 -95
  32. pyobs_gui-2.0.0.dev4/DEV_IAutoFocus.md +0 -162
  33. pyobs_gui-2.0.0.dev4/DEV_IAutoGuiding.md +0 -156
  34. pyobs_gui-2.0.0.dev4/DEV_NavbarShortcuts.md +0 -286
  35. pyobs_gui-2.0.0.dev4/DEV_qml_evaluation.md +0 -218
  36. pyobs_gui-2.0.0.dev4/DEV_telescopewidget_layout.md +0 -199
  37. pyobs_gui-2.0.0.dev4/pyobs_gui/gui.py +0 -92
  38. pyobs_gui-2.0.0.dev4/specs/README.md +0 -11
  39. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/.github/dependabot.yml +0 -0
  40. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/.github/workflows/pypi.yml +0 -0
  41. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/.github/workflows/pyrefly.yml +0 -0
  42. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/.github/workflows/ruff.yml +0 -0
  43. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/.pre-commit-config.yaml +0 -0
  44. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/.readthedocs.yml +0 -0
  45. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/CHANGELOG.rst +0 -0
  46. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/LICENSE +0 -0
  47. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/README.md +0 -0
  48. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/bin/mypyuic5 +0 -0
  49. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/docs/Makefile +0 -0
  50. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/docs/make.bat +0 -0
  51. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/docs/requirements.txt +0 -0
  52. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/docs/source/_static/pyobs.gif +0 -0
  53. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/docs/source/conf.py +0 -0
  54. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/docs/source/index.rst +0 -0
  55. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/mypy.ini +0 -0
  56. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/acquisitionwidget.py +0 -0
  57. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/autofocuswidget.py +0 -0
  58. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/autoguidingwidget.py +0 -0
  59. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/base.py +0 -0
  60. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/camerawidget.py +0 -0
  61. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/commandinputwidget.py +0 -0
  62. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/coolingwidget.py +0 -0
  63. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/datadisplaywidget.py +0 -0
  64. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/eventswidget.py +0 -0
  65. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/filterwidget.py +0 -0
  66. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/fitsheaderswidget.py +0 -0
  67. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/focuswidget.py +0 -0
  68. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/modewidget.py +0 -0
  69. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/modifiedmixin.py +0 -0
  70. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/modulegui.py +0 -0
  71. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/__init__.py +0 -0
  72. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  73. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  74. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  75. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  76. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  77. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  78. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/camerawidget.ui +0 -0
  79. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  80. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  81. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/compassmovewidget.py +0 -0
  82. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  83. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  84. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/compile.sh +0 -0
  85. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/coolingwidget.ui +0 -0
  86. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  87. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  88. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  89. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  90. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/designer.sh +0 -0
  91. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/eventswidget.ui +0 -0
  92. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  93. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/filterwidget.ui +0 -0
  94. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  95. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  96. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  97. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/focuswidget.ui +0 -0
  98. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  99. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/modewidget.ui +0 -0
  100. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/modewidget_ui.py +0 -0
  101. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  102. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  103. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  104. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  105. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  106. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  107. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  108. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  109. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  110. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  111. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  112. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  113. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources.qrc +0 -0
  114. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/resources_rc.py +0 -0
  115. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/roofwidget.ui +0 -0
  116. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  117. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/shellwidget.ui +0 -0
  118. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  119. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  120. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  121. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/telescopewidget.ui +0 -0
  122. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  123. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  124. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  125. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  126. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  127. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/videowidget.ui +0 -0
  128. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/videowidget_ui.py +0 -0
  129. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/weatherwidget.ui +0 -0
  130. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  131. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/roofwidget.py +0 -0
  132. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/shellwidget.py +0 -0
  133. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/spectrographwidget.py +0 -0
  134. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/temperaturesplotwidget.py +0 -0
  135. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/temperatureswidget.py +0 -0
  136. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/videowidget.py +0 -0
  137. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/pyobs_gui/watchedlabel.py +0 -0
  138. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/acquisition.yaml +0 -0
  139. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/autofocus.yaml +0 -0
  140. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/camera.yaml +0 -0
  141. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/full.yaml +0 -0
  142. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/guiding.yaml +0 -0
  143. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/mode.yaml +0 -0
  144. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/roof.yaml +0 -0
  145. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/spectrograph.yaml +0 -0
  146. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/telescope.yaml +0 -0
  147. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/telescope_acl.yaml +0 -0
  148. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/telescope_acl_denied.yaml +0 -0
  149. {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev6}/test/video.yaml +0 -0
@@ -6,7 +6,7 @@ description: Drive the real pyobs-gui app headlessly against a real pyobs module
6
6
  # Verifying pyobs-gui changes
7
7
 
8
8
  pyobs-gui is a PySide6 app that talks to pyobs modules over `Comm`. There's no
9
- pytest suite (see `DEVELOPMENT.md`) — verification means running the real
9
+ pytest suite — verification means running the real
10
10
  `GUI` module against a real target module and observing widget state /
11
11
  screenshots. This works headlessly, no Xvfb needed.
12
12
 
@@ -0,0 +1,13 @@
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
9
+
10
+ # pyside6-deploy build output for the real app -- generated, not source
11
+ deployment/
12
+ dist/
13
+ build*.log
@@ -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.dev6
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
@@ -4,6 +4,10 @@ TODO: write doc
4
4
 
5
5
  __title__ = "GUI"
6
6
 
7
+ from ._nuitka_astropy_patch import patch_generic_unit_parser
8
+
9
+ patch_generic_unit_parser()
10
+
7
11
  from .gui import GUI
8
12
  from .camerawidget import CameraWidget
9
13
  from .datadisplaywidget import DataDisplayWidget
@@ -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
+
10
+ def main() -> None:
11
+ from pyobs.application import Application
12
+
13
+ from .gui import GUI
14
+ from .login import login_and_build_gui
15
+
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,453 @@
1
+ """Work around a Nuitka/astropy incompatibility that breaks unit-string parsing in the
2
+ compiled standalone binary (see specs/plans/gui-widget-plugins-and-packaging.md).
3
+
4
+ astropy.units.format.generic builds its PLY grammar by having ``astropy.extern.ply.yacc``/
5
+ ``lex`` walk the call stack with ``sys._getframe()`` to auto-discover the ``tokens``/``p_*``/
6
+ ``t_*`` local variables defined inside ``Generic._parser``/``Generic._lexer``. Nuitka-compiled
7
+ function frames don't populate ``f_locals`` the way CPython's interpreted frames do, so that
8
+ walk finds no grammar and the very first ``import astropy.units`` (pulled in unconditionally by
9
+ pyobs-core's ``pyobs.utils.enums``) crashes with ``YaccError: Unable to build parser``. This is a
10
+ real, unresolved upstream limitation as of 2026-07, not a pyobs bug:
11
+ https://github.com/astropy/astropy/issues/15069
12
+ https://github.com/Nuitka/Nuitka/issues/2313
13
+
14
+ Fix: patch ``astropy.extern.ply.yacc.get_caller_module_dict``/``...lex.get_caller_module_dict``
15
+ (the two functions actually doing the broken frame walk) so that whenever the walk comes back
16
+ without a ``tokens`` entry, they fall back to a namespace we rebuild here from the same grammar
17
+ (copied verbatim from astropy.units.format.generic), handed to PLY in a way that doesn't need
18
+ frame introspection at all.
19
+
20
+ This has to patch at the ``astropy.extern.ply`` level, not by importing
21
+ ``astropy.units.format.generic`` and monkeypatching ``Generic`` directly, because of a
22
+ bootstrapping deadlock: merely importing ``astropy.units.core``/``astropy.units.format.generic``
23
+ runs ``astropy/units/__init__.py``, which (via ``astropy.units.astrophys``, defining physical
24
+ constants) *already* triggers the broken parser during that very import -- before a class-level
25
+ patch could ever be installed. Patching ``get_caller_module_dict`` needs only
26
+ ``astropy.extern.ply``, which has no dependency on ``astropy.units`` at all, so it's safe to
27
+ install before anything touches ``astropy.units``. The vendored grammar's own reference to
28
+ ``astropy.units.core.Unit``/``CompositeUnit``/``Generic`` is resolved lazily, only when the
29
+ fallback actually fires -- by then we're already being called from inside
30
+ ``Generic._parser``/``_lexer``'s own call chain, so those names are guaranteed importable.
31
+
32
+ Scoped to the generic unit format only -- the one format pyobs_gui is known to hit
33
+ unconditionally at startup. astropy's cds/ogip/vounit formats and astropy.coordinates.angles use
34
+ the same PLY pattern and would need the identical treatment if/when they're actually exercised in
35
+ the compiled binary.
36
+
37
+ Must run before ``astropy.units`` is imported anywhere, i.e. as the first thing in
38
+ ``pyobs_gui/__init__.py``.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import re
44
+ import tempfile
45
+ import types
46
+ from fractions import Fraction
47
+ from pathlib import Path
48
+ from typing import Any
49
+
50
+ _generic_ns_cache: dict[str, types.ModuleType] = {}
51
+
52
+
53
+ def _build_generic_grammar_namespace() -> types.ModuleType:
54
+ """Rebuild astropy.units.format.generic's PLY grammar as a plain namespace object.
55
+
56
+ Only called lazily, from inside the ``get_caller_module_dict`` fallback below -- by that
57
+ point ``astropy.units.core``/``astropy.units.format.generic`` are already far enough along
58
+ to import from (see module docstring).
59
+ """
60
+ from astropy.units.core import CompositeUnit, Unit
61
+ from astropy.units.format.generic import Generic
62
+
63
+ cls = Generic
64
+ tokens = cls._tokens
65
+
66
+ # astropy.utils.parsing wants to write a generated tab-file cache next to __file__'s
67
+ # directory. In a frozen binary, astropy/units/format/ isn't a real directory on disk (it's
68
+ # compiled into the binary), so point this at a writable scratch directory instead --
69
+ # writing there is harmless best-effort caching, not required for parsing to work.
70
+ stub_dir = Path(tempfile.gettempdir()) / "pyobs_gui_astropy_ply_stub"
71
+ stub_dir.mkdir(parents=True, exist_ok=True)
72
+
73
+ ns = types.ModuleType("_pyobs_gui_generic_grammar")
74
+ ns.__file__ = str(stub_dir / "generic.py")
75
+ ns.tokens = tokens
76
+
77
+ ns.t_COMMA = r"\,"
78
+ ns.t_PRODUCT = "[*.]"
79
+ ns.t_DIVISION = "/"
80
+ ns.t_POWER = r"\^|(\*\*)"
81
+ ns.t_OPEN_PAREN = r"\("
82
+ ns.t_CLOSE_PAREN = r"\)"
83
+
84
+ def t_UFLOAT(t):
85
+ r"((\d+\.?\d*)|(\.\d+))([eE][+-]?\d+)?"
86
+ if not re.search(r"[eE\.]", t.value):
87
+ t.type = "UINT"
88
+ t.value = int(t.value)
89
+ elif t.value.endswith("."):
90
+ t.type = "UINT"
91
+ t.value = int(t.value[:-1])
92
+ else:
93
+ t.value = float(t.value)
94
+ return t
95
+
96
+ def t_UINT(t):
97
+ r"\d+"
98
+ t.value = int(t.value)
99
+ return t
100
+
101
+ def t_SIGN(t):
102
+ r"[+-](?=\d)"
103
+ t.value = int(t.value + "1")
104
+ return t
105
+
106
+ def t_FUNCNAME(t):
107
+ r"((sqrt)|(ln)|(exp)|(log)|(mag)|(dB)|(dex))(?=\ *\()"
108
+ return t
109
+
110
+ def t_UNIT(t):
111
+ r"[^\s\d+\-\./\*\^\(\)\,]+"
112
+ t.value = cls._get_unit(t)
113
+ return t
114
+
115
+ ns.t_ignore = " "
116
+
117
+ def t_error(t):
118
+ raise ValueError(f"Invalid character at col {t.lexpos}")
119
+
120
+ ns.t_UFLOAT = t_UFLOAT
121
+ ns.t_UINT = t_UINT
122
+ ns.t_SIGN = t_SIGN
123
+ ns.t_FUNCNAME = t_FUNCNAME
124
+ ns.t_UNIT = t_UNIT
125
+ ns.t_error = t_error
126
+
127
+ def p_main(p):
128
+ """
129
+ main : unit
130
+ | structured_unit
131
+ | structured_subunit
132
+ """
133
+ if isinstance(p[1], tuple):
134
+ p[0] = p[1][0]
135
+ else:
136
+ p[0] = p[1]
137
+
138
+ def p_structured_subunit(p):
139
+ """
140
+ structured_subunit : OPEN_PAREN structured_unit CLOSE_PAREN
141
+ """
142
+ p[0] = (p[2],)
143
+
144
+ def p_structured_unit(p):
145
+ """
146
+ structured_unit : subunit COMMA
147
+ | subunit COMMA subunit
148
+ """
149
+ from astropy.units.structured import StructuredUnit
150
+
151
+ inputs = (p[1],) if len(p) == 3 else (p[1], p[3])
152
+ units = ()
153
+ for subunit in inputs:
154
+ if isinstance(subunit, tuple):
155
+ units += subunit
156
+ elif isinstance(subunit, StructuredUnit):
157
+ units += subunit.values()
158
+ else:
159
+ units += (subunit,)
160
+
161
+ p[0] = StructuredUnit(units)
162
+
163
+ def p_subunit(p):
164
+ """
165
+ subunit : unit
166
+ | structured_unit
167
+ | structured_subunit
168
+ """
169
+ p[0] = p[1]
170
+
171
+ def p_unit(p):
172
+ """
173
+ unit : product_of_units
174
+ | factor product_of_units
175
+ | factor PRODUCT product_of_units
176
+ | division_product_of_units
177
+ | factor division_product_of_units
178
+ | factor PRODUCT division_product_of_units
179
+ | inverse_unit
180
+ | factor inverse_unit
181
+ | factor PRODUCT inverse_unit
182
+ | factor
183
+ """
184
+ if len(p) == 2:
185
+ p[0] = Unit(p[1])
186
+ elif len(p) == 3:
187
+ p[0] = CompositeUnit(p[1] * p[2].scale, p[2].bases, p[2].powers)
188
+ elif len(p) == 4:
189
+ p[0] = CompositeUnit(p[1] * p[3].scale, p[3].bases, p[3].powers)
190
+
191
+ def p_division_product_of_units(p):
192
+ """
193
+ division_product_of_units : division_product_of_units DIVISION product_of_units
194
+ | product_of_units
195
+ """
196
+ if len(p) == 4:
197
+ p[0] = Unit(p[1] / p[3])
198
+ else:
199
+ p[0] = p[1]
200
+
201
+ def p_inverse_unit(p):
202
+ """
203
+ inverse_unit : DIVISION unit_expression
204
+ """
205
+ p[0] = p[2] ** -1
206
+
207
+ def p_factor(p):
208
+ """
209
+ factor : factor_fits
210
+ | factor_float
211
+ | factor_int
212
+ """
213
+ p[0] = p[1]
214
+
215
+ def p_factor_float(p):
216
+ """
217
+ factor_float : signed_float
218
+ | signed_float UINT signed_int
219
+ | signed_float UINT POWER numeric_power
220
+ """
221
+ if cls.name == "fits":
222
+ raise ValueError("Numeric factor not supported by FITS")
223
+ if len(p) == 4:
224
+ p[0] = p[1] * p[2] ** float(p[3])
225
+ elif len(p) == 5:
226
+ p[0] = p[1] * p[2] ** float(p[4])
227
+ elif len(p) == 2:
228
+ p[0] = p[1]
229
+
230
+ def p_factor_int(p):
231
+ """
232
+ factor_int : UINT
233
+ | UINT signed_int
234
+ | UINT POWER numeric_power
235
+ | UINT UINT signed_int
236
+ | UINT UINT POWER numeric_power
237
+ """
238
+ if cls.name == "fits":
239
+ raise ValueError("Numeric factor not supported by FITS")
240
+ if len(p) == 2:
241
+ p[0] = p[1]
242
+ elif len(p) == 3:
243
+ p[0] = p[1] ** float(p[2])
244
+ elif len(p) == 4:
245
+ if isinstance(p[2], int):
246
+ p[0] = p[1] * p[2] ** float(p[3])
247
+ else:
248
+ p[0] = p[1] ** float(p[3])
249
+ elif len(p) == 5:
250
+ p[0] = p[1] * p[2] ** p[4]
251
+
252
+ def p_factor_fits(p):
253
+ """
254
+ factor_fits : UINT POWER OPEN_PAREN signed_int CLOSE_PAREN
255
+ | UINT POWER OPEN_PAREN UINT CLOSE_PAREN
256
+ | UINT POWER signed_int
257
+ | UINT POWER UINT
258
+ | UINT SIGN UINT
259
+ | UINT OPEN_PAREN signed_int CLOSE_PAREN
260
+ """
261
+ if p[1] != 10:
262
+ if cls.name == "fits":
263
+ raise ValueError("Base must be 10")
264
+ else:
265
+ return
266
+ if len(p) == 4:
267
+ if p[2] in ("**", "^"):
268
+ p[0] = 10 ** p[3]
269
+ else:
270
+ p[0] = 10 ** (p[2] * p[3])
271
+ elif len(p) == 5:
272
+ p[0] = 10 ** p[3]
273
+ elif len(p) == 6:
274
+ p[0] = 10 ** p[4]
275
+
276
+ def p_product_of_units(p):
277
+ """
278
+ product_of_units : unit_expression PRODUCT product_of_units
279
+ | unit_expression product_of_units
280
+ | unit_expression
281
+ """
282
+ if len(p) == 2:
283
+ p[0] = p[1]
284
+ elif len(p) == 3:
285
+ p[0] = p[1] * p[2]
286
+ else:
287
+ p[0] = p[1] * p[3]
288
+
289
+ def p_unit_expression(p):
290
+ """
291
+ unit_expression : function
292
+ | unit_with_power
293
+ | OPEN_PAREN product_of_units CLOSE_PAREN
294
+ """
295
+ if len(p) == 2:
296
+ p[0] = p[1]
297
+ else:
298
+ p[0] = p[2]
299
+
300
+ def p_unit_with_power(p):
301
+ """
302
+ unit_with_power : UNIT POWER numeric_power
303
+ | UNIT numeric_power
304
+ | UNIT
305
+ """
306
+ if len(p) == 2:
307
+ p[0] = p[1]
308
+ elif len(p) == 3:
309
+ p[0] = p[1] ** p[2]
310
+ else:
311
+ p[0] = p[1] ** p[3]
312
+
313
+ def p_numeric_power(p):
314
+ """
315
+ numeric_power : sign UINT
316
+ | OPEN_PAREN paren_expr CLOSE_PAREN
317
+ """
318
+ if len(p) == 3:
319
+ p[0] = p[1] * p[2]
320
+ elif len(p) == 4:
321
+ p[0] = p[2]
322
+
323
+ def p_paren_expr(p):
324
+ """
325
+ paren_expr : sign UINT
326
+ | signed_float
327
+ | frac
328
+ """
329
+ if len(p) == 3:
330
+ p[0] = p[1] * p[2]
331
+ else:
332
+ p[0] = p[1]
333
+
334
+ def p_frac(p):
335
+ """
336
+ frac : sign UINT DIVISION sign UINT
337
+ """
338
+ p[0] = Fraction(p[1] * p[2], p[4] * p[5])
339
+
340
+ def p_sign(p):
341
+ """
342
+ sign : SIGN
343
+ |
344
+ """
345
+ if len(p) == 2:
346
+ p[0] = p[1]
347
+ else:
348
+ p[0] = 1
349
+
350
+ def p_signed_int(p):
351
+ """
352
+ signed_int : SIGN UINT
353
+ """
354
+ p[0] = p[1] * p[2]
355
+
356
+ def p_signed_float(p):
357
+ """
358
+ signed_float : sign UINT
359
+ | sign UFLOAT
360
+ """
361
+ p[0] = p[1] * p[2]
362
+
363
+ def p_function(p):
364
+ """
365
+ function : FUNCNAME OPEN_PAREN main CLOSE_PAREN
366
+ """
367
+ if p[1] == "sqrt":
368
+ p[0] = p[3] ** 0.5
369
+ return
370
+ elif p[1] in ("mag", "dB", "dex"):
371
+ function_unit = cls._validate_unit(p[1])
372
+ if callable(function_unit):
373
+ p[0] = function_unit(p[3])
374
+ return
375
+
376
+ raise ValueError(f"'{p[1]}' is not a recognized function")
377
+
378
+ def p_error(p):
379
+ raise ValueError()
380
+
381
+ ns.p_main = p_main
382
+ ns.p_structured_subunit = p_structured_subunit
383
+ ns.p_structured_unit = p_structured_unit
384
+ ns.p_subunit = p_subunit
385
+ ns.p_unit = p_unit
386
+ ns.p_division_product_of_units = p_division_product_of_units
387
+ ns.p_inverse_unit = p_inverse_unit
388
+ ns.p_factor = p_factor
389
+ ns.p_factor_float = p_factor_float
390
+ ns.p_factor_int = p_factor_int
391
+ ns.p_factor_fits = p_factor_fits
392
+ ns.p_product_of_units = p_product_of_units
393
+ ns.p_unit_expression = p_unit_expression
394
+ ns.p_unit_with_power = p_unit_with_power
395
+ ns.p_numeric_power = p_numeric_power
396
+ ns.p_paren_expr = p_paren_expr
397
+ ns.p_frac = p_frac
398
+ ns.p_sign = p_sign
399
+ ns.p_signed_int = p_signed_int
400
+ ns.p_signed_float = p_signed_float
401
+ ns.p_function = p_function
402
+ ns.p_error = p_error
403
+
404
+ return ns
405
+
406
+
407
+ def _namespace_pdict(ns: types.ModuleType) -> dict[str, Any]:
408
+ return {k: getattr(ns, k) for k in dir(ns)}
409
+
410
+
411
+ def patch_generic_unit_parser() -> None:
412
+ """Patch astropy.extern.ply's frame-walking helpers to fall back to our vendored grammar.
413
+
414
+ Safe to call unconditionally (frozen or not) -- under a normal, unfrozen interpreter the
415
+ original frame walk always succeeds, so the fallback path never triggers.
416
+ """
417
+ from astropy.extern.ply import lex as ply_lex
418
+ from astropy.extern.ply import yacc as ply_yacc
419
+
420
+ original_yacc_dict = ply_yacc.get_caller_module_dict
421
+ original_lex_dict = ply_lex.get_caller_module_dict
422
+
423
+ def _fallback_pdict(pdict: dict[str, Any] | None) -> dict[str, Any]:
424
+ # Keep whatever the real (possibly broken-locals) pdict already has -- __file__/
425
+ # __package__/__name__ are module globals and come through fine even on a compiled
426
+ # frame -- and only supplement the missing tokens/p_*/t_* grammar entries. Replacing
427
+ # __file__ wholesale would point astropy's tab-file bookkeeping at the wrong directory.
428
+ if "ns" not in _generic_ns_cache:
429
+ _generic_ns_cache["ns"] = _build_generic_grammar_namespace()
430
+ merged = dict(pdict) if pdict else {}
431
+ merged.update(_namespace_pdict(_generic_ns_cache["ns"]))
432
+ return merged
433
+
434
+ def patched_yacc_dict(levels: int) -> dict[str, Any]:
435
+ try:
436
+ pdict = original_yacc_dict(levels)
437
+ except Exception:
438
+ return _fallback_pdict(None)
439
+ if "tokens" not in pdict:
440
+ return _fallback_pdict(pdict)
441
+ return pdict
442
+
443
+ def patched_lex_dict(levels: int) -> dict[str, Any]:
444
+ try:
445
+ pdict = original_lex_dict(levels)
446
+ except Exception:
447
+ return _fallback_pdict(None)
448
+ if "tokens" not in pdict:
449
+ return _fallback_pdict(pdict)
450
+ return pdict
451
+
452
+ ply_yacc.get_caller_module_dict = patched_yacc_dict
453
+ ply_lex.get_caller_module_dict = patched_lex_dict
@@ -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,