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.
- pyobs_gui-2.0.0.dev5/.gitignore +8 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/PKG-INFO +4 -2
- pyobs_gui-2.0.0.dev5/pyobs_gui/__main__.py +21 -0
- pyobs_gui-2.0.0.dev5/pyobs_gui/accounts.py +150 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/compassmovewidget.py +2 -0
- pyobs_gui-2.0.0.dev5/pyobs_gui/gui.py +173 -0
- pyobs_gui-2.0.0.dev5/pyobs_gui/login.py +64 -0
- pyobs_gui-2.0.0.dev5/pyobs_gui/loginwindow.py +377 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/logmodel.py +11 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/mainwindow.py +89 -22
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/mainwindow.ui +195 -115
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/mainwindow_ui.py +75 -20
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/statuswidget.py +26 -2
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/telescopewidget.py +19 -2
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/utils.py +15 -2
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/weatherwidget.py +6 -4
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyproject.toml +12 -4
- pyobs_gui-2.0.0.dev5/specs/README.md +20 -0
- pyobs_gui-2.0.0.dev5/spike_standalone/main.py +84 -0
- pyobs_gui-2.0.0.dev5/spike_standalone/pysidedeploy.spec +97 -0
- pyobs_gui-2.0.0.dev5/tests/__init__.py +0 -0
- pyobs_gui-2.0.0.dev5/tests/conftest.py +20 -0
- pyobs_gui-2.0.0.dev5/tests/test_accounts.py +106 -0
- pyobs_gui-2.0.0.dev5/tests/test_loginwindow.py +196 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/uv.lock +275 -154
- pyobs_gui-2.0.0.dev4/pyobs_gui/gui.py +0 -92
- pyobs_gui-2.0.0.dev4/specs/README.md +0 -11
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.claude/skills/verify/SKILL.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.github/dependabot.yml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.github/workflows/pypi.yml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.github/workflows/pyrefly.yml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.github/workflows/ruff.yml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.pre-commit-config.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/.readthedocs.yml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/CHANGELOG.rst +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEVELOPMENT.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_IAcquisition.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_IAutoFocus.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_IAutoGuiding.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_NavbarShortcuts.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_qml_evaluation.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/DEV_telescopewidget_layout.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/LICENSE +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/README.md +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/bin/mypyuic5 +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/Makefile +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/make.bat +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/requirements.txt +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/source/_static/pyobs.gif +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/source/conf.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/docs/source/index.rst +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/mypy.ini +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/acquisitionwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/autofocuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/autoguidingwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/base.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/camerawidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/commandinputwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/coolingwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/datadisplaywidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/eventswidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/filterwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/fitsheaderswidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/focuswidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modewidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modifiedmixin.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/modulegui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/__init__.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autofocuswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/camerawidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/camerawidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmoveplugin.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/compile.sh +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/coolingwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplayplugin.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/designer.sh +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/eventswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/eventswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/filterwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/filterwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/focuswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/focuswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/modewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/modewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
- {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
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/search-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources.qrc +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/resources_rc.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/roofwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/roofwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/shellwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/shellwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/spectrographwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/telescopewidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperatureswidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/videowidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/videowidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/weatherwidget.ui +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/roofwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/shellwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/spectrographwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/temperaturesplotwidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/temperatureswidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/videowidget.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/pyobs_gui/watchedlabel.py +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/acquisition.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/autofocus.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/camera.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/full.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/guiding.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/mode.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/roof.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/spectrograph.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/telescope.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/telescope_acl.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/telescope_acl_denied.yaml +0 -0
- {pyobs_gui-2.0.0.dev4 → pyobs_gui-2.0.0.dev5}/test/video.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyobs-gui
|
|
3
|
-
Version: 2.0.0.
|
|
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.
|
|
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"]
|