arkitekt-next 0.7.8__py3-none-any.whl

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.

Potentially problematic release.


This version of arkitekt-next might be problematic. Click here for more details.

Files changed (119) hide show
  1. arkitekt_next/__init__.py +43 -0
  2. arkitekt_next/apps/__init__.py +3 -0
  3. arkitekt_next/apps/easy.py +99 -0
  4. arkitekt_next/apps/next.py +40 -0
  5. arkitekt_next/apps/qt.py +97 -0
  6. arkitekt_next/apps/service/__init__.py +3 -0
  7. arkitekt_next/apps/service/fakts.py +88 -0
  8. arkitekt_next/apps/service/fakts_next.py +79 -0
  9. arkitekt_next/apps/service/fakts_qt.py +82 -0
  10. arkitekt_next/apps/service/fluss_next.py +31 -0
  11. arkitekt_next/apps/service/grant_registry.py +27 -0
  12. arkitekt_next/apps/service/herre.py +24 -0
  13. arkitekt_next/apps/service/herre_qt.py +57 -0
  14. arkitekt_next/apps/service/kabinet.py +31 -0
  15. arkitekt_next/apps/service/mikro_next.py +81 -0
  16. arkitekt_next/apps/service/rekuest_next.py +53 -0
  17. arkitekt_next/apps/service/unlok_next.py +32 -0
  18. arkitekt_next/apps/types.py +53 -0
  19. arkitekt_next/builders.py +264 -0
  20. arkitekt_next/cli/__init__.py +0 -0
  21. arkitekt_next/cli/commands/call/__init__.py +0 -0
  22. arkitekt_next/cli/commands/call/local.py +132 -0
  23. arkitekt_next/cli/commands/call/main.py +22 -0
  24. arkitekt_next/cli/commands/call/remote.py +90 -0
  25. arkitekt_next/cli/commands/gen/__init__.py +0 -0
  26. arkitekt_next/cli/commands/gen/compile.py +45 -0
  27. arkitekt_next/cli/commands/gen/init.py +122 -0
  28. arkitekt_next/cli/commands/gen/main.py +29 -0
  29. arkitekt_next/cli/commands/gen/watch.py +32 -0
  30. arkitekt_next/cli/commands/init/__init__.py +0 -0
  31. arkitekt_next/cli/commands/init/main.py +194 -0
  32. arkitekt_next/cli/commands/inspect/__init__.py +0 -0
  33. arkitekt_next/cli/commands/inspect/definitions.py +53 -0
  34. arkitekt_next/cli/commands/inspect/main.py +22 -0
  35. arkitekt_next/cli/commands/inspect/variables.py +92 -0
  36. arkitekt_next/cli/commands/manifest/__init__.py +0 -0
  37. arkitekt_next/cli/commands/manifest/inspect.py +42 -0
  38. arkitekt_next/cli/commands/manifest/main.py +25 -0
  39. arkitekt_next/cli/commands/manifest/scopes.py +155 -0
  40. arkitekt_next/cli/commands/manifest/version.py +147 -0
  41. arkitekt_next/cli/commands/manifest/wizard.py +94 -0
  42. arkitekt_next/cli/commands/port/__init__.py +0 -0
  43. arkitekt_next/cli/commands/port/build.py +231 -0
  44. arkitekt_next/cli/commands/port/init.py +82 -0
  45. arkitekt_next/cli/commands/port/main.py +31 -0
  46. arkitekt_next/cli/commands/port/publish.py +102 -0
  47. arkitekt_next/cli/commands/port/stage.py +59 -0
  48. arkitekt_next/cli/commands/port/utils.py +47 -0
  49. arkitekt_next/cli/commands/port/validate.py +78 -0
  50. arkitekt_next/cli/commands/port/wizard.py +329 -0
  51. arkitekt_next/cli/commands/run/__init__.py +0 -0
  52. arkitekt_next/cli/commands/run/dev.py +349 -0
  53. arkitekt_next/cli/commands/run/main.py +22 -0
  54. arkitekt_next/cli/commands/run/prod.py +57 -0
  55. arkitekt_next/cli/commands/run/utils.py +10 -0
  56. arkitekt_next/cli/commands/server/__init__.py +0 -0
  57. arkitekt_next/cli/commands/server/down.py +56 -0
  58. arkitekt_next/cli/commands/server/init.py +74 -0
  59. arkitekt_next/cli/commands/server/inspect.py +59 -0
  60. arkitekt_next/cli/commands/server/main.py +33 -0
  61. arkitekt_next/cli/commands/server/open.py +66 -0
  62. arkitekt_next/cli/commands/server/remove.py +60 -0
  63. arkitekt_next/cli/commands/server/stop.py +56 -0
  64. arkitekt_next/cli/commands/server/up.py +70 -0
  65. arkitekt_next/cli/commands/server/utils.py +33 -0
  66. arkitekt_next/cli/configs/base.yaml +867 -0
  67. arkitekt_next/cli/constants.py +63 -0
  68. arkitekt_next/cli/dockerfiles/vanilla.dockerfile +8 -0
  69. arkitekt_next/cli/errors.py +4 -0
  70. arkitekt_next/cli/inspect.py +1 -0
  71. arkitekt_next/cli/io.py +255 -0
  72. arkitekt_next/cli/main.py +83 -0
  73. arkitekt_next/cli/options.py +166 -0
  74. arkitekt_next/cli/schemas/fluss.schema.graphql +2446 -0
  75. arkitekt_next/cli/schemas/gucker.schema.graphql +8908 -0
  76. arkitekt_next/cli/schemas/kabinet.schema.graphql +515 -0
  77. arkitekt_next/cli/schemas/kluster.schema.graphql +109 -0
  78. arkitekt_next/cli/schemas/konviktion.schema.graphql +70 -0
  79. arkitekt_next/cli/schemas/kuay.schema.graphql +356 -0
  80. arkitekt_next/cli/schemas/mikro.schema.graphql +8908 -0
  81. arkitekt_next/cli/schemas/mikro_next.schema.graphql +1639 -0
  82. arkitekt_next/cli/schemas/napari.schema.graphql +8908 -0
  83. arkitekt_next/cli/schemas/omero_ark.schema.graphql +100 -0
  84. arkitekt_next/cli/schemas/port.schema.graphql +356 -0
  85. arkitekt_next/cli/schemas/rekuest.schema.graphql +4630 -0
  86. arkitekt_next/cli/schemas/rekuest_next.schema.graphql +1159 -0
  87. arkitekt_next/cli/schemas/unlok.schema.graphql +1013 -0
  88. arkitekt_next/cli/templates/filter.py +26 -0
  89. arkitekt_next/cli/templates/simple.py +67 -0
  90. arkitekt_next/cli/texts.py +20 -0
  91. arkitekt_next/cli/types.py +365 -0
  92. arkitekt_next/cli/ui.py +111 -0
  93. arkitekt_next/cli/utils.py +15 -0
  94. arkitekt_next/cli/validators.py +17 -0
  95. arkitekt_next/cli/vars.py +39 -0
  96. arkitekt_next/cli/versions/v1.yaml +1 -0
  97. arkitekt_next/constants.py +6 -0
  98. arkitekt_next/model.py +110 -0
  99. arkitekt_next/qt/__init__.py +9 -0
  100. arkitekt_next/qt/assets/dark/gear.png +0 -0
  101. arkitekt_next/qt/assets/dark/green pulse.gif +0 -0
  102. arkitekt_next/qt/assets/dark/orange pulse.gif +0 -0
  103. arkitekt_next/qt/assets/dark/pink pulse.gif +0 -0
  104. arkitekt_next/qt/assets/dark/red pulse.gif +0 -0
  105. arkitekt_next/qt/assets/light/gear.png +0 -0
  106. arkitekt_next/qt/assets/light/green pulse.gif +0 -0
  107. arkitekt_next/qt/assets/light/orange pulse.gif +0 -0
  108. arkitekt_next/qt/assets/light/pink pulse.gif +0 -0
  109. arkitekt_next/qt/assets/light/red pulse.gif +0 -0
  110. arkitekt_next/qt/magic_bar.py +545 -0
  111. arkitekt_next/qt/utils.py +30 -0
  112. arkitekt_next/service_registry.py +51 -0
  113. arkitekt_next/tqdm.py +43 -0
  114. arkitekt_next/utils.py +38 -0
  115. arkitekt_next-0.7.8.dist-info/LICENSE +21 -0
  116. arkitekt_next-0.7.8.dist-info/METADATA +155 -0
  117. arkitekt_next-0.7.8.dist-info/RECORD +119 -0
  118. arkitekt_next-0.7.8.dist-info/WHEEL +4 -0
  119. arkitekt_next-0.7.8.dist-info/entry_points.txt +3 -0
arkitekt_next/model.py ADDED
@@ -0,0 +1,110 @@
1
+ """Models for ArkitektNext. Thiese include extensiosn for the Fakts Manifest and the User model."""
2
+
3
+ from hashlib import sha256
4
+ from pydantic import BaseModel, Field
5
+ from typing import List, Optional
6
+
7
+
8
+ class Requirement(BaseModel):
9
+ service: str
10
+ """ The service is the service that will be used to fill the key, it will be used to find the correct instance. It needs to fullfill
11
+ the reverse domain naming scheme"""
12
+ optional: bool = False
13
+ """ The optional flag indicates if the requirement is optional or not. Users should be able to use the client even if the requirement is not met. """
14
+ description: Optional[str] = None
15
+ """ The description is a human readable description of the requirement. Will be show to the user when asking for the requirement."""
16
+
17
+
18
+ def build_default_requirements() -> dict[str, Requirement]:
19
+ return {
20
+ "lok": Requirement(
21
+ service="live.arkitekt_next.lok",
22
+ description="An instance of ArkitektNext Lok to authenticate the user",
23
+ ),
24
+ "rekuest": Requirement(
25
+ service="live.arkitekt_next.rekuest",
26
+ description="An instance of ArkitektNext Rekuest to assign to nodes",
27
+ ),
28
+ "kabinet": Requirement(
29
+ service="live.arkitekt_next.kabinet",
30
+ description="An instance of ArkitektNext Kabinet to retrieve nodes from",
31
+ ),
32
+ "mikro": Requirement(
33
+ service="live.arkitekt_next.mikro",
34
+ description="An instance of ArkitektNext Mikro to make requests to the user's data",
35
+ optional=True,
36
+ ),
37
+ "fluss": Requirement(
38
+ service="live.arkitekt_next.fluss",
39
+ description="An instance of ArkitektNext Fluss to make requests to the user's data",
40
+ optional=False,
41
+ ),
42
+ "port": Requirement(
43
+ service="live.arkitekt_next.port",
44
+ description="An instance of ArkitektNext Fluss to make requests to the user's data",
45
+ optional=True,
46
+ ),
47
+ "datalayer": Requirement(
48
+ service="live.arkitekt_next.datalayer",
49
+ description="An instance of ArkitektNext Datalayer to make requests to the user's data",
50
+ optional=False,
51
+ ),
52
+ }
53
+
54
+
55
+ class Manifest(BaseModel):
56
+ """A manifest for an app that can be installed in ArkitektNext
57
+
58
+ Manifests are used to describe apps that can be installed in ArkitektNext.
59
+ They provide information about the app, such as the
60
+ its globally unique identifier, the version, the scopes it needs, etc.
61
+
62
+ This Manifest is send to the Fakts server on initial app configuration,
63
+ and is used to register the app with the Fakts server, which in turn
64
+ will prompt the user to grant the app access to establish itself as
65
+ an ArkitektNext app (and therefore as an OAuth2 client) (see more in the
66
+ Fakts documentation).
67
+
68
+ """
69
+
70
+ version: str
71
+ """ The version of the app TODO: Should this be a semver? """
72
+ identifier: str
73
+ """ The globally unique identifier of the app: TODO: Should we check for a reverse domain name? """
74
+ scopes: List[str]
75
+ """ Scopes that this app should request from the user """
76
+ logo: Optional[str]
77
+ """ A URL to the logo of the app TODO: We should enforce this to be a http URL as local paths won't work """
78
+ requirements: Optional[dict[str, Requirement]] = Field(
79
+ default_factory=build_default_requirements
80
+ )
81
+ """ Requirements that this app has TODO: What are the requirements? """
82
+
83
+ class Config:
84
+ extra = "forbid"
85
+
86
+
87
+ def hash(self):
88
+ """Hash the manifest"""
89
+ return sha256(self.json(sort_keys=True).encode()).hexdigest()
90
+
91
+ class User(BaseModel):
92
+ """A user of ArkitektNext
93
+
94
+ This model represents a user on ArkitektNext. As herre is acgnostic to the
95
+ user model, we need to provide a model that can be used to represent
96
+ the ArkitektNext user. This model is used by the
97
+ :class:`herre.fakts.fakts_endpoint_fetcher.FaktsUserFetcher` to
98
+ fetch the user from the associated ArkitektNext Lok instance. This model
99
+ is closely mimicking the OIDC user model, and is therefore compatible
100
+ to represent OIDC users.
101
+
102
+ """
103
+
104
+ id: str = Field(alias="sub")
105
+ """ The user's id (in lok, this is the user's sub(ject) ID)"""
106
+
107
+ username: str = Field(alias="preferred_username")
108
+ """ The user's preferred username """
109
+ email: str = Field(alias="email")
110
+ """ The user's preferred username """
@@ -0,0 +1,9 @@
1
+ """Qt related modules.
2
+
3
+ This module contains Modules that are Qt related, and help to integrate
4
+ ArkitektNext with Qt applications.
5
+
6
+ The main component is the MagicBar, which is a widget that can be added
7
+ to any Qt application. It will then allow the user to configure and connect
8
+ to ArkitektNext, and configure settings.
9
+ """
Binary file
Binary file
@@ -0,0 +1,545 @@
1
+ from enum import Enum
2
+ from qtpy import QtWidgets, QtGui, QtCore
3
+ from koil.qt import async_to_qt
4
+
5
+ from arkitekt_next.apps.types import App
6
+ from .utils import get_image_path
7
+ from typing import Optional, Callable
8
+ import logging
9
+ import aiohttp
10
+ from logging import LogRecord
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class Logo(QtWidgets.QWidget):
16
+ """Logo widget
17
+
18
+ THhe logo widget is a widget that can be used to display a logo in the settings dialog.
19
+ It will download the logo from the url, and display it.
20
+
21
+
22
+ """
23
+
24
+ def __init__(self, url: str, *args, **kwargs) -> None:
25
+ """Logo widget
26
+
27
+ Parameters
28
+ ----------
29
+ url : str
30
+ The url to download the logo from.
31
+ """
32
+
33
+ super().__init__(*args, **kwargs)
34
+ self.logo_url = url
35
+ self.getter = async_to_qt(
36
+ self.aget_image,
37
+ ) # we use async_to_qt to convert the async function to a Qt signal. (see koil docs)
38
+
39
+ self.mylayout = QtWidgets.QVBoxLayout()
40
+ self.setLayout(self.mylayout)
41
+ self.getter.returned.connect(self.on_image)
42
+ self.getter.run()
43
+
44
+ def on_image(self, data: Optional[bytes]) -> None:
45
+ """Callback for when the image is downloaded."""
46
+ if not data:
47
+ return
48
+
49
+ self.pixmap = QtGui.QPixmap()
50
+ self.pixmap.loadFromData(data)
51
+ self.scaled_pixmap = self.pixmap.scaledToWidth(100)
52
+ self.logo = QtWidgets.QLabel()
53
+ self.logo.setPixmap(self.scaled_pixmap)
54
+
55
+ self.mylayout.addWidget(self.logo)
56
+
57
+ async def aget_image(self) -> Optional[bytes]:
58
+ """Async function to download the image."""
59
+ async with aiohttp.ClientSession() as session:
60
+ async with session.get(self.logo_url) as resp:
61
+ if resp.status == 200 and "image" in resp.headers["Content-Type"]:
62
+ data = await resp.read()
63
+ return data
64
+ else:
65
+ print(f"Failed to download the image. Status code: {resp.status}")
66
+ return None
67
+
68
+
69
+ class ArkitektNextLogsRetriever(logging.Handler, QtCore.QObject):
70
+ """A logging handler that will emit a Qt signal when a log message is received."""
71
+
72
+ appendPlainText = QtCore.Signal(str)
73
+
74
+ def __init__(self, widget: QtWidgets.QPlainTextEdit, *args, **kwargs) -> None:
75
+ """A logging handler that will emit a Qt signal when a log message is received.
76
+
77
+ Parameters
78
+ ----------
79
+ widget : QtWidgets.QPlainTextEdit
80
+ A plain text edit widget to display the logs in.
81
+ """
82
+ super().__init__()
83
+ QtCore.QObject.__init__(self)
84
+ self.appendPlainText.connect(widget.appendPlainText)
85
+ self.setFormatter(
86
+ logging.Formatter(
87
+ "%(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s"
88
+ )
89
+ )
90
+
91
+ def emit(self, record: LogRecord) -> None:
92
+ """Emit a Qt signal when a log message is received."""
93
+ msg = self.format(record)
94
+ self.appendPlainText.emit(msg)
95
+
96
+
97
+ class ArkitektNextLogs(QtWidgets.QDialog):
98
+ """A dialog that will display the logs of the app."""
99
+
100
+ def __init__(
101
+ self,
102
+ settings: QtCore.QSettings,
103
+ *args,
104
+ log_level_key: str = "log_level",
105
+ log_to_file_key: str = "log_to_file",
106
+ **kwargs,
107
+ ) -> None:
108
+ """A dialog that will display the logs of the app.
109
+
110
+ Parameters
111
+ ----------
112
+ settings : QtCore.QSettings
113
+ The settings object to use to store the log level. (so that is persistent,
114
+ and can be changed by the user)
115
+ log_level_key : str, optional
116
+ The key to use to store the log level, by default "log_level"
117
+ log_to_file_key : str, optional
118
+ The key to use to store whether the logs should be written to a file,
119
+ by default "log_to_file"
120
+ """
121
+ super().__init__(*args, **kwargs)
122
+ self.log_level_key = log_level_key
123
+ self.log_to_file_key = log_to_file_key
124
+ self.settings = settings
125
+ self.setWindowTitle("Logs")
126
+ self.mylayout = QtWidgets.QVBoxLayout()
127
+ self.text = QtWidgets.QPlainTextEdit(parent=self)
128
+ self.text.setMaximumBlockCount(5000)
129
+ self.text.setReadOnly(True)
130
+ self.mylayout.addWidget(self.text)
131
+ self.logRetriever = ArkitektNextLogsRetriever(self.text)
132
+ logging.getLogger().addHandler(self.logRetriever)
133
+ logging.getLogger().setLevel(self.log_level)
134
+ self.setLayout(self.mylayout)
135
+
136
+ def update_log_level(self, level: str) -> None:
137
+ """Update the log level.
138
+
139
+ Parameters
140
+ ----------
141
+ level : str
142
+ Update the log level to this level.
143
+ """
144
+ logging.getLogger().setLevel(level)
145
+ self.settings.setValue(self.log_level_key, level)
146
+
147
+ @property
148
+ def log_to_file(self) -> bool:
149
+ """Should the logs be written to a file."""
150
+ return self.settings.value(self.log_to_file_key, False, bool)
151
+
152
+ @property
153
+ def log_level(self) -> str:
154
+ """The log level in use."""
155
+ return self.settings.value(self.log_level_key, "INFO", str)
156
+
157
+
158
+ class Profile(QtWidgets.QDialog):
159
+ """The profile dialog.
160
+
161
+ It will display the logo, and allow the user to change the user, and server.
162
+ It will also allow the user to change the log level, and show the logs on
163
+ demand.
164
+
165
+
166
+ """
167
+
168
+ updated = QtCore.Signal()
169
+
170
+ def __init__(
171
+ self,
172
+ app: App,
173
+ bar: "MagicBar",
174
+ *args,
175
+ dark_mode: bool = False,
176
+ **kwargs,
177
+ ) -> None:
178
+ """The profile dialog.
179
+
180
+ Parameters
181
+ ----------
182
+ app : QtApp
183
+ The app to use. (needs to be a QtApp as it uses Qt signals)
184
+ bar : MagicBar
185
+ The magic bar to use and to update when the user changes the settings.
186
+ dark_mode : bool, optional
187
+ Should we use dark_mode, by default False
188
+ TODO: implement dark mode
189
+ """
190
+ super().__init__(*args, **{"parent": bar, **kwargs})
191
+ self.app = app
192
+ self.bar = bar
193
+
194
+ self.settings = QtCore.QSettings(
195
+ "arkitekt_next",
196
+ f"{self.app.manifest.identifier}:{self.app.manifest.version}:profile",
197
+ )
198
+
199
+ self.setWindowTitle("Settings")
200
+
201
+ self.infobar = QtWidgets.QVBoxLayout()
202
+
203
+ self.mylayout = QtWidgets.QHBoxLayout()
204
+ self.setLayout(self.mylayout)
205
+ self.mylayout.addLayout(self.infobar)
206
+
207
+ if self.app.manifest.logo:
208
+ self.infobar.addWidget(Logo(self.app.manifest.logo, parent=self))
209
+
210
+ self.infobar.addWidget(QtWidgets.QLabel(self.app.manifest.identifier))
211
+ self.infobar.addWidget(QtWidgets.QLabel(self.app.manifest.version))
212
+
213
+ self.logout_button = QtWidgets.QPushButton("Change User")
214
+ self.logout_button.clicked.connect(
215
+ lambda: self.bar.refresh_token_task.run(
216
+ allow_cache=False,
217
+ allow_refresh=False,
218
+ allow_auto_login=False,
219
+ delete_active=True,
220
+ )
221
+ )
222
+
223
+ self.unkonfigure_button = QtWidgets.QPushButton("Change Server")
224
+ self.unkonfigure_button.clicked.connect(
225
+ lambda: self.bar.refresh_task.run(
226
+ allow_auto_demand=False,
227
+ allow_auto_discover=False,
228
+ delete_active=True,
229
+ )
230
+ )
231
+
232
+ button_bar = QtWidgets.QHBoxLayout()
233
+ self.infobar.addLayout(button_bar)
234
+ button_bar.addWidget(self.logout_button)
235
+ button_bar.addWidget(self.unkonfigure_button)
236
+
237
+ self.logs = ArkitektNextLogs(self.settings, parent=self)
238
+
239
+ self.go_all_the_way_button = QtWidgets.QPushButton("One click provide")
240
+ self.go_all_the_way_button.setCheckable(True)
241
+ self.go_all_the_way_button.setChecked(self.go_all_the_way_down)
242
+ self.go_all_the_way_button.clicked.connect(self.on_go_all_the_way_clicked)
243
+
244
+ self.sidebar = QtWidgets.QVBoxLayout()
245
+ self.mylayout.addLayout(self.sidebar)
246
+
247
+ self.show_logs_button = QtWidgets.QPushButton("Show Logs")
248
+ self.show_logs_button.clicked.connect(self.logs.show)
249
+
250
+ self.sidebar.addWidget(self.go_all_the_way_button)
251
+ self.sidebar.addWidget(self.show_logs_button)
252
+ self.sidebar.addStretch()
253
+
254
+ def on_go_all_the_way_clicked(self, checked: bool) -> None:
255
+ """Callback for when the go all the way button is clicked.
256
+
257
+ Will update the settings, and emit the updated signal.
258
+
259
+ Parameters
260
+ ----------
261
+ checked : bool
262
+ Whether the button is checked or not.
263
+
264
+ """
265
+ self.settings.setValue("go_all_the_way_down", checked)
266
+ self.updated.emit()
267
+
268
+ @property
269
+ def go_all_the_way_down(self) -> bool:
270
+ """Should the app go all the way down when the user clicks the magic button."""
271
+ return self.settings.value("go_all_the_way_down", True, bool)
272
+
273
+
274
+ class AppState(str, Enum):
275
+ """The state of the app."""
276
+
277
+ READY = "ready"
278
+ DOWN = "down"
279
+ UP = "up"
280
+
281
+
282
+ class ProcessState(str, Enum):
283
+ UNKONFIGURED = "unkonfigured"
284
+ UNLOGGED = "unlogged"
285
+ UNPROVIDED = "unprovided"
286
+ PROVIDING = "providing"
287
+
288
+
289
+ class MagicBar(QtWidgets.QWidget):
290
+ """Magic bar widget.
291
+
292
+ The magic bar is a small button widget, that can be used to configure, login and put the
293
+ app in providing and non providing states.. It also has a gear button that opens the
294
+ profile dialog. To adjust some parameters of the app"""
295
+
296
+ CONNECT_LABEL = "Connect"
297
+
298
+ app_state_changed = QtCore.Signal()
299
+ app_up = QtCore.Signal()
300
+ app_down = QtCore.Signal()
301
+ app_error = QtCore.Signal()
302
+ state = AppState.DOWN
303
+ process_state = ProcessState.UNKONFIGURED
304
+
305
+ def __init__(
306
+ self,
307
+ app: App,
308
+ dark_mode: bool = False,
309
+ on_error: Optional[Callable[[Exception], None]] = None,
310
+ ) -> None:
311
+ """Magic bar Widget
312
+
313
+ This widget is a small button widget, that can be used to configure, login and put the
314
+ app in providing and non providing states.. It also has a gear button that opens the
315
+ profile dialog, that can be used to adjust some parameters of the qt app.
316
+
317
+ Parameters
318
+ ----------
319
+ app : QtApp
320
+ A qt app to use.
321
+ dark_mode : bool, optional
322
+ Should we use the dark mode, by default False
323
+ on_error : Optional[Callable[[Exception], None]], optional
324
+ And additinal callback if an error is raised, by default None
325
+ """
326
+ super().__init__()
327
+ self.app = app
328
+
329
+ # assert isinstance(
330
+ # self.app.koil, QtPedanticKoil
331
+ # ), f"Koil should be Qt Koil but is {type(self.app.koil)}"
332
+ self.dark_mode = dark_mode
333
+
334
+ self.profile = Profile(app, self, dark_mode=dark_mode)
335
+ self.profile.updated.connect(self.on_profile_updated)
336
+
337
+ self.configure_task = async_to_qt(self.app.fakts.aget)
338
+ self.configure_task.errored.connect(self.configure_errored)
339
+ self.configure_task.returned.connect(self.set_unlogined)
340
+
341
+ self.refresh_task = async_to_qt(self.app.fakts.arefresh)
342
+ self.refresh_task.errored.connect(self.configure_errored)
343
+ self.refresh_task.returned.connect(self.set_unlogined)
344
+
345
+ self.get_token_task = async_to_qt(self.app.herre.aget_token)
346
+ self.get_token_task.errored.connect(self.login_errored)
347
+ self.get_token_task.returned.connect(self.set_unprovided)
348
+
349
+ self.refresh_token_task = async_to_qt(self.app.herre.arefresh_token)
350
+ self.refresh_token_task.started.connect(self.set_providing)
351
+ self.refresh_token_task.errored.connect(self.login_errored)
352
+ self.refresh_token_task.returned.connect(self.set_unprovided)
353
+
354
+ self.provide_task = async_to_qt(self.app.rekuest.agent.aprovide)
355
+ self.provide_task.errored.connect(self.provide_errored)
356
+ self.provide_task.returned.connect(self.set_unprovided)
357
+
358
+ self.magicb = QtWidgets.QPushButton(MagicBar.CONNECT_LABEL)
359
+ self.magicb.setMinimumHeight(30)
360
+ self.magicb.setMaximumHeight(30)
361
+
362
+ self.configure_future = None
363
+ self.login_future = None
364
+ self.provide_future = None
365
+
366
+ self.mylayout = QtWidgets.QHBoxLayout()
367
+ self.gearb_pix = QtGui.QPixmap(get_image_path("gear.png", dark_mode=dark_mode))
368
+ self.gearb = QtWidgets.QPushButton()
369
+ self.gearb.setIcon(QtGui.QIcon(self.gearb_pix))
370
+ self.gearb.setMinimumWidth(30)
371
+ self.gearb.setMaximumWidth(30)
372
+ self.gearb.setMinimumHeight(30)
373
+ self.gearb.setMaximumHeight(30)
374
+ self._on_error = on_error
375
+
376
+ self.magicb.clicked.connect(self.magic_button_clicked)
377
+ self.gearb.clicked.connect(self.gear_button_clicked)
378
+
379
+ self.mylayout.addWidget(self.magicb)
380
+ self.mylayout.addWidget(self.gearb)
381
+ self.setLayout(self.mylayout)
382
+
383
+ self.set_unkonfigured()
384
+ self.on_profile_updated()
385
+
386
+ def on_profile_updated(self) -> None:
387
+ """Callback for when the profile is updated."""
388
+ if self.profile.go_all_the_way_down:
389
+ self.set_unprovided()
390
+
391
+ def show_error(self, ex: Exception) -> None:
392
+ """Show an error message
393
+
394
+ Parameters
395
+ ----------
396
+ ex : Exception
397
+ The exception to show.
398
+ """
399
+ if self._on_error:
400
+ self._on_error(ex)
401
+ else:
402
+ logger.error(f"Error {repr(ex)}")
403
+
404
+ def task_errored(self, ex: Exception) -> None:
405
+ """_summary_
406
+
407
+ Parameters
408
+ ----------
409
+ ex : Exception
410
+ _description_
411
+
412
+ Raises
413
+ ------
414
+ ex
415
+ _description_
416
+ """
417
+ raise ex
418
+
419
+ def configure_errored(self, ex: Exception) -> None:
420
+ self.set_unkonfigured()
421
+ self.show_error(ex)
422
+
423
+ def login_errored(self, ex: Exception) -> None:
424
+ self.set_unlogined()
425
+ self.show_error(ex)
426
+
427
+ def provide_errored(self, ex: Exception) -> None:
428
+ self.set_unprovided()
429
+ self.show_error(ex)
430
+
431
+ def on_configured(self) -> None:
432
+ self.magicb.setText("Login")
433
+
434
+ def on_login(self) -> None:
435
+ self.magicb.setText("Provide")
436
+
437
+ def on_provided(self) -> None:
438
+ self.magicb.setText("Provide ended")
439
+
440
+ def on_providing_ended(self) -> None:
441
+ pass
442
+
443
+ def gear_button_clicked(self) -> None:
444
+ self.profile.show()
445
+
446
+ def update_movie(self) -> None:
447
+ self.magicb.setIcon(QtGui.QIcon(self.magicb_movie.currentPixmap()))
448
+
449
+ def set_button_movie(self, movie) -> None:
450
+ self.magicb_movie = QtGui.QMovie(
451
+ get_image_path(movie, dark_mode=self.dark_mode)
452
+ )
453
+ self.magicb_movie.frameChanged.connect(self.update_movie)
454
+ self.magicb_movie.setScaledSize(QtCore.QSize(30, 30))
455
+ self.magicb_movie.start()
456
+
457
+ def set_unkonfigured(self) -> None:
458
+ self.state = AppState.DOWN
459
+ self.process_state = ProcessState.UNKONFIGURED
460
+ self.app_down.emit()
461
+ self.app_state_changed.emit()
462
+ self.set_button_movie("pink pulse.gif")
463
+ self.profile.unkonfigure_button.setDisabled(True)
464
+ self.profile.logout_button.setDisabled(True)
465
+ self.magicb.setDisabled(False)
466
+ self.magicb.setText("Konfigure App")
467
+
468
+ def set_unlogined(self) -> None:
469
+ self.state = AppState.DOWN
470
+ self.process_state = ProcessState.UNLOGGED
471
+ self.app_down.emit()
472
+
473
+ self.app_state_changed.emit()
474
+ self.set_button_movie("orange pulse.gif")
475
+ self.profile.unkonfigure_button.setDisabled(False)
476
+ self.profile.logout_button.setDisabled(True)
477
+ self.magicb.setDisabled(False)
478
+ self.magicb.setText("Login")
479
+
480
+ def set_unprovided(self) -> None:
481
+ self.state = AppState.UP
482
+ self.process_state = ProcessState.UNPROVIDED
483
+ self.app_up.emit()
484
+
485
+ self.app_state_changed.emit()
486
+ self.set_button_movie("green pulse.gif")
487
+ self.profile.unkonfigure_button.setDisabled(False)
488
+ self.profile.logout_button.setDisabled(False)
489
+ self.magicb.setDisabled(False)
490
+ self.magicb.setText("Provide")
491
+
492
+ def set_providing(self) -> None:
493
+ self.state = AppState.UP
494
+ self.process_state = ProcessState.PROVIDING
495
+ self.app_up.emit()
496
+
497
+ self.app_state_changed.emit()
498
+ self.set_button_movie("red pulse.gif")
499
+ self.profile.unkonfigure_button.setDisabled(False)
500
+ self.profile.logout_button.setDisabled(False)
501
+ self.magicb.setDisabled(False)
502
+ self.magicb.setText("Cancel Provide..")
503
+
504
+ def magic_button_clicked(self) -> None:
505
+ if (
506
+ self.process_state == ProcessState.UNKONFIGURED
507
+ and not self.profile.go_all_the_way_down
508
+ ):
509
+ if not self.configure_future or self.configure_future.done():
510
+ self.configure_future = self.configure_task.run()
511
+ self.magicb.setText("Cancel Configuration")
512
+ return
513
+ if not self.configure_future.done():
514
+ self.configure_future.cancel()
515
+ self.set_unkonfigured()
516
+ return
517
+
518
+ if (
519
+ self.process_state == ProcessState.UNLOGGED
520
+ and not self.profile.go_all_the_way_down
521
+ ):
522
+ if not self.login_future or self.login_future.done():
523
+ self.login_future = self.get_token_task.run()
524
+ self.magicb.setText("Cancel Login")
525
+ return
526
+ if not self.login_future.done():
527
+ self.login_future.cancel()
528
+ self.magicb.setText("Login")
529
+ self.set_unlogined()
530
+ return
531
+
532
+ if (
533
+ self.process_state != ProcessState.PROVIDING
534
+ or self.profile.go_all_the_way_down
535
+ ):
536
+ if not self.provide_future or self.provide_future.done():
537
+ self.provide_future = self.provide_task.run()
538
+ self.set_providing()
539
+ return
540
+
541
+ if self.provide_future:
542
+ if not self.provide_future.done():
543
+ self.provide_future.cancel()
544
+ self.set_unprovided()
545
+ return