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.
- arkitekt_next/__init__.py +43 -0
- arkitekt_next/apps/__init__.py +3 -0
- arkitekt_next/apps/easy.py +99 -0
- arkitekt_next/apps/next.py +40 -0
- arkitekt_next/apps/qt.py +97 -0
- arkitekt_next/apps/service/__init__.py +3 -0
- arkitekt_next/apps/service/fakts.py +88 -0
- arkitekt_next/apps/service/fakts_next.py +79 -0
- arkitekt_next/apps/service/fakts_qt.py +82 -0
- arkitekt_next/apps/service/fluss_next.py +31 -0
- arkitekt_next/apps/service/grant_registry.py +27 -0
- arkitekt_next/apps/service/herre.py +24 -0
- arkitekt_next/apps/service/herre_qt.py +57 -0
- arkitekt_next/apps/service/kabinet.py +31 -0
- arkitekt_next/apps/service/mikro_next.py +81 -0
- arkitekt_next/apps/service/rekuest_next.py +53 -0
- arkitekt_next/apps/service/unlok_next.py +32 -0
- arkitekt_next/apps/types.py +53 -0
- arkitekt_next/builders.py +264 -0
- arkitekt_next/cli/__init__.py +0 -0
- arkitekt_next/cli/commands/call/__init__.py +0 -0
- arkitekt_next/cli/commands/call/local.py +132 -0
- arkitekt_next/cli/commands/call/main.py +22 -0
- arkitekt_next/cli/commands/call/remote.py +90 -0
- arkitekt_next/cli/commands/gen/__init__.py +0 -0
- arkitekt_next/cli/commands/gen/compile.py +45 -0
- arkitekt_next/cli/commands/gen/init.py +122 -0
- arkitekt_next/cli/commands/gen/main.py +29 -0
- arkitekt_next/cli/commands/gen/watch.py +32 -0
- arkitekt_next/cli/commands/init/__init__.py +0 -0
- arkitekt_next/cli/commands/init/main.py +194 -0
- arkitekt_next/cli/commands/inspect/__init__.py +0 -0
- arkitekt_next/cli/commands/inspect/definitions.py +53 -0
- arkitekt_next/cli/commands/inspect/main.py +22 -0
- arkitekt_next/cli/commands/inspect/variables.py +92 -0
- arkitekt_next/cli/commands/manifest/__init__.py +0 -0
- arkitekt_next/cli/commands/manifest/inspect.py +42 -0
- arkitekt_next/cli/commands/manifest/main.py +25 -0
- arkitekt_next/cli/commands/manifest/scopes.py +155 -0
- arkitekt_next/cli/commands/manifest/version.py +147 -0
- arkitekt_next/cli/commands/manifest/wizard.py +94 -0
- arkitekt_next/cli/commands/port/__init__.py +0 -0
- arkitekt_next/cli/commands/port/build.py +231 -0
- arkitekt_next/cli/commands/port/init.py +82 -0
- arkitekt_next/cli/commands/port/main.py +31 -0
- arkitekt_next/cli/commands/port/publish.py +102 -0
- arkitekt_next/cli/commands/port/stage.py +59 -0
- arkitekt_next/cli/commands/port/utils.py +47 -0
- arkitekt_next/cli/commands/port/validate.py +78 -0
- arkitekt_next/cli/commands/port/wizard.py +329 -0
- arkitekt_next/cli/commands/run/__init__.py +0 -0
- arkitekt_next/cli/commands/run/dev.py +349 -0
- arkitekt_next/cli/commands/run/main.py +22 -0
- arkitekt_next/cli/commands/run/prod.py +57 -0
- arkitekt_next/cli/commands/run/utils.py +10 -0
- arkitekt_next/cli/commands/server/__init__.py +0 -0
- arkitekt_next/cli/commands/server/down.py +56 -0
- arkitekt_next/cli/commands/server/init.py +74 -0
- arkitekt_next/cli/commands/server/inspect.py +59 -0
- arkitekt_next/cli/commands/server/main.py +33 -0
- arkitekt_next/cli/commands/server/open.py +66 -0
- arkitekt_next/cli/commands/server/remove.py +60 -0
- arkitekt_next/cli/commands/server/stop.py +56 -0
- arkitekt_next/cli/commands/server/up.py +70 -0
- arkitekt_next/cli/commands/server/utils.py +33 -0
- arkitekt_next/cli/configs/base.yaml +867 -0
- arkitekt_next/cli/constants.py +63 -0
- arkitekt_next/cli/dockerfiles/vanilla.dockerfile +8 -0
- arkitekt_next/cli/errors.py +4 -0
- arkitekt_next/cli/inspect.py +1 -0
- arkitekt_next/cli/io.py +255 -0
- arkitekt_next/cli/main.py +83 -0
- arkitekt_next/cli/options.py +166 -0
- arkitekt_next/cli/schemas/fluss.schema.graphql +2446 -0
- arkitekt_next/cli/schemas/gucker.schema.graphql +8908 -0
- arkitekt_next/cli/schemas/kabinet.schema.graphql +515 -0
- arkitekt_next/cli/schemas/kluster.schema.graphql +109 -0
- arkitekt_next/cli/schemas/konviktion.schema.graphql +70 -0
- arkitekt_next/cli/schemas/kuay.schema.graphql +356 -0
- arkitekt_next/cli/schemas/mikro.schema.graphql +8908 -0
- arkitekt_next/cli/schemas/mikro_next.schema.graphql +1639 -0
- arkitekt_next/cli/schemas/napari.schema.graphql +8908 -0
- arkitekt_next/cli/schemas/omero_ark.schema.graphql +100 -0
- arkitekt_next/cli/schemas/port.schema.graphql +356 -0
- arkitekt_next/cli/schemas/rekuest.schema.graphql +4630 -0
- arkitekt_next/cli/schemas/rekuest_next.schema.graphql +1159 -0
- arkitekt_next/cli/schemas/unlok.schema.graphql +1013 -0
- arkitekt_next/cli/templates/filter.py +26 -0
- arkitekt_next/cli/templates/simple.py +67 -0
- arkitekt_next/cli/texts.py +20 -0
- arkitekt_next/cli/types.py +365 -0
- arkitekt_next/cli/ui.py +111 -0
- arkitekt_next/cli/utils.py +15 -0
- arkitekt_next/cli/validators.py +17 -0
- arkitekt_next/cli/vars.py +39 -0
- arkitekt_next/cli/versions/v1.yaml +1 -0
- arkitekt_next/constants.py +6 -0
- arkitekt_next/model.py +110 -0
- arkitekt_next/qt/__init__.py +9 -0
- arkitekt_next/qt/assets/dark/gear.png +0 -0
- arkitekt_next/qt/assets/dark/green pulse.gif +0 -0
- arkitekt_next/qt/assets/dark/orange pulse.gif +0 -0
- arkitekt_next/qt/assets/dark/pink pulse.gif +0 -0
- arkitekt_next/qt/assets/dark/red pulse.gif +0 -0
- arkitekt_next/qt/assets/light/gear.png +0 -0
- arkitekt_next/qt/assets/light/green pulse.gif +0 -0
- arkitekt_next/qt/assets/light/orange pulse.gif +0 -0
- arkitekt_next/qt/assets/light/pink pulse.gif +0 -0
- arkitekt_next/qt/assets/light/red pulse.gif +0 -0
- arkitekt_next/qt/magic_bar.py +545 -0
- arkitekt_next/qt/utils.py +30 -0
- arkitekt_next/service_registry.py +51 -0
- arkitekt_next/tqdm.py +43 -0
- arkitekt_next/utils.py +38 -0
- arkitekt_next-0.7.8.dist-info/LICENSE +21 -0
- arkitekt_next-0.7.8.dist-info/METADATA +155 -0
- arkitekt_next-0.7.8.dist-info/RECORD +119 -0
- arkitekt_next-0.7.8.dist-info/WHEEL +4 -0
- 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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|