1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-03-04 16:02:51 +01:00

feat(bec-atlas-admin-view): add http service through QNetworkAccessManager

This commit is contained in:
2026-02-02 11:17:04 +01:00
parent 345e5afab0
commit 7cb1db20d8

View File

@@ -0,0 +1,178 @@
import json
from pydantic import BaseModel
from qtpy.QtCore import QUrl, Signal
from qtpy.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from qtpy.QtWidgets import QMessageBox, QWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.services.bec_atlas_admin_view.login_dialog import LoginDialog
class HTTPResponse(BaseModel):
request_url: str
headers: dict
status: int
data: dict | list | str
class BECAtlasHTTPService(QWidget):
"""HTTP service using the QNetworkAccessManager to interact with the BEC Atlas API."""
http_response_received = Signal(dict)
authenticated = Signal(bool)
def __init__(self, parent=None, base_url: str = "", headers: dict | None = None):
super().__init__(parent)
if headers is None:
headers = {"accept": "application/json"}
self._headers = headers
self._base_url = base_url
self.network_manager = QNetworkAccessManager(self)
self.network_manager.finished.connect(self._handle_response)
self._authenticated = False
def closeEvent(self, event):
self.cleanup()
return super().closeEvent(event)
def cleanup(self):
"""Cleanup connection, destroy authenticate cookies."""
# Disconnect signals to avoid handling responses after cleanup
self.network_manager.finished.disconnect(self._handle_response)
# Logout to invalidate session on server side
self.logout()
# Delete all cookies related to the base URL
for cookie in self.network_manager.cookieJar().cookiesForUrl(QUrl(self._base_url)):
self.network_manager.cookieJar().deleteCookie(cookie)
def _handle_response(self, reply: QNetworkReply):
"""
Handle the HTTP response from the server.
Args:
reply (QNetworkReply): The network reply object containing the response.
"""
status = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
raw_bytes = bytes(reply.readAll())
request_url = reply.url().toString()
headers = dict(reply.rawHeaderPairs())
reply.deleteLater()
if "login" in request_url and status == 200:
self._authenticated = True
self.authenticated.emit(True)
elif "logout" in request_url and status == 200:
self._authenticated = False
self.authenticated.emit(False)
# TODO, should we handle failures here or rather on more high levels?
if status == 401:
if "login" in request_url:
# Failed login attempt
self._show_warning(
title="Login Failed", text="Please check your login credentials."
)
else:
self._show_warning(
title="Unauthorized",
text="You are not authorized to request this information. Please authenticate first.",
)
return
self._handle_raw_response(raw_bytes, status, request_url, headers)
def _handle_raw_response(self, raw_bytes: bytes, status: int, request_url: str, headers: dict):
try:
if len(raw_bytes) > 0:
data = json.loads(raw_bytes.decode("utf-8"))
else:
data = {}
except Exception:
data = {}
response = HTTPResponse(request_url=request_url, headers=headers, status=status, data=data)
self.http_response_received.emit(response.model_dump())
def _show_warning(self, title: str, text: str):
"""Show a warning message box for unauthorized access."""
QMessageBox.warning(self, title, text, QMessageBox.StandardButton.Ok)
def _show_login(self):
"""Show the login dialog to enter credentials."""
dlg = LoginDialog(parent=self)
dlg.credentials_entered.connect(self._set_credentials)
dlg.exec_() # blocking here is OK for login
def _set_credentials(self, username: str, password: str):
"""Set the credentials and perform login."""
self.post_request("/user/login", {"username": username, "password": password})
################
# HTTP Methods
################
def get_request(self, endpoint: str):
"""
GET request to the API endpoint.
Args:
endpoint (str): The API endpoint to send the GET request to.
"""
url = QUrl(self._base_url + endpoint)
request = QNetworkRequest(url)
for key, value in self._headers.items():
request.setRawHeader(key.encode("utf-8"), value.encode("utf-8"))
self.network_manager.get(request)
def post_request(self, endpoint: str, payload: dict):
"""
POST request to the API endpoint with a JSON payload.
Args:
endpoint (str): The API endpoint to send the POST request to.
payload (dict): The JSON payload to include in the POST request.
"""
url = QUrl(self._base_url + endpoint)
request = QNetworkRequest(url)
# Headers
for key, value in self._headers.items():
request.setRawHeader(key.encode("utf-8"), value.encode("utf-8"))
request.setHeader(QNetworkRequest.KnownHeaders.ContentTypeHeader, "application/json")
payload_dump = json.dumps(payload).encode("utf-8")
reply = self.network_manager.post(request, payload_dump)
reply.finished.connect(lambda: self._handle_reply(reply))
################
# API Methods
################
@SafeSlot()
def login(self):
"""Login to BEC Atlas with the provided username and password."""
# TODO should we prompt here if already authenticated - and add option to skip login otherwise first destroy old token and re-authenticate?
self._show_login()
def logout(self):
"""Logout from BEC Atlas."""
self.post_request("/user/logout", {})
def check_health(self):
"""Check the health status of BEC Atlas."""
self.get_request("/health")
def get_realms(self, include_deployments: bool = True):
"""Get the list of realms from BEC Atlas. Requires authentication."""
if not self._authenticated:
self._show_login()
# Requires authentication
endpoint = "/realms"
if include_deployments:
endpoint += "?include_deployments=true"
self.get_request(endpoint)