Skip to content

Commit

Permalink
Network requests now report their errors back to main UI elements (Ge…
Browse files Browse the repository at this point in the history
…oNode#149)

Co-authored-by: Ricardo Garcia Silva <[email protected]>
  • Loading branch information
Samweli and Ricardo Garcia Silva authored Apr 7, 2021
1 parent 14c8d5f commit a335000
Show file tree
Hide file tree
Showing 3 changed files with 401 additions and 211 deletions.
294 changes: 158 additions & 136 deletions src/qgis_geonode/apiclient/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,143 +21,10 @@
from .models import GeonodeApiSearchParameters


@dataclasses.dataclass()
class EventLoopResult:
result: typing.Optional[bool]


@contextmanager
def wait_for_signal(
signal, timeout: int = 10000
) -> typing.ContextManager[EventLoopResult]:
"""Fire up a custom event loop and wait for the input signal to be emitted
This function allows running QT async code in a blocking fashion. It works by
spawning a Qt event loop. This custom loop has its `quit()` slot bound to the
input `signal`. The event loop is `exec_`'ed, thus blocking the current
thread until the the input `signal` is emitted.
"""

loop = QtCore.QEventLoop()
signal.connect(loop.quit)
loop_result = EventLoopResult(result=None)
yield loop_result
QtCore.QTimer.singleShot(timeout, partial(_forcibly_terminate_loop, loop))
log(f"About to start custom event loop...")
loop_result.result = not bool(loop.exec_())
log(f"Custom event loop ended, resuming...")


def _forcibly_terminate_loop(loop: QtCore.QEventLoop):
log("Forcibly ending event loop...")
loop.exit(1)


def reply_matches(
qgis_reply: qgis.core.QgsNetworkReplyContent, qt_reply: QtNetwork.QNetworkReply
) -> bool:
reply_id = int(qt_reply.property("requestId"))
return qgis_reply.requestId() == reply_id


class NetworkFetcherTask(qgis.core.QgsTask):
authcfg: typing.Optional[str]
description: str
request: QtNetwork.QNetworkRequest
request_payload: typing.Optional[str]
reply_content: typing.Optional[QtCore.QByteArray]
parsed_reply: typing.Optional[ParsedNetworkReply]
redirect_policy: QtNetwork.QNetworkRequest.RedirectPolicy
_reply: typing.Optional[QtNetwork.QNetworkReply]

request_finished = QtCore.pyqtSignal()
request_parsed = QtCore.pyqtSignal()

def __init__(
self,
request: QtNetwork.QNetworkRequest,
request_payload: typing.Optional[str] = None,
authcfg: typing.Optional[str] = None,
description: typing.Optional[str] = "MyNetworkfetcherTask",
redirect_policy: typing.Optional[QtNetwork.QNetworkRequest.RedirectPolicy] = (
QtNetwork.QNetworkRequest.NoLessSafeRedirectPolicy
),
):
"""
Custom QgsTask that performs network requests
This class is able to perform both GET and POST HTTP requests.
It is needed because:
- QgsNetworkContentFetcherTask only performs GET requests
- QgsNetworkAcessManager.blockingPost() does not seem to handle redirects
correctly
Implementation is based on QgsNetworkContentFetcher. The run() method performs
a normal async request using QtNetworkAccessManager's get() or post() methods.
The resulting QNetworkReply instance has its `finished` signal be connected to
a custom handler. The request is executed in scope of a custom Qt event loop,
which blocks the current thread while the request is being processed.
"""

super().__init__(description)
self.authcfg = authcfg
self.request = request
self.request_payload = request_payload
self.reply_content = None
self.parsed_reply = None
self.redirect_policy = redirect_policy
self.network_access_manager = qgis.core.QgsNetworkAccessManager.instance()
self.network_access_manager.setRedirectPolicy(self.redirect_policy)
self.network_access_manager.finished.connect(self._request_done)
self._reply = None

def run(self):
if self.authcfg is not None:
auth_manager = qgis.core.QgsApplication.authManager()
auth_manager.updateNetworkRequest(self.request, self.authcfg)
with wait_for_signal(self.request_parsed):
if self.request_payload is None:
self._reply = self.network_access_manager.get(self.request)
else:
self._reply = self.network_access_manager.post(
self.request,
QtCore.QByteArray(self.request_payload.encode("utf-8")),
)
try:
result = self.parsed_reply.qt_error is None
self._reply.deleteLater()
self._reply = None
except AttributeError:
result = False
self.network_access_manager.finished.disconnect(self._request_done)
self.request_finished.emit()
return result

def _request_done(self, qgis_reply: qgis.core.QgsNetworkReplyContent):
log(f"requested_url: {qgis_reply.request().url().toString()}")
if self._reply is None:
log(
"Some other request was completed, probably authentication, "
"ignoring..."
)
elif reply_matches(qgis_reply, self._reply):
self.reply_content = self._reply.readAll()
self.parsed_reply = parse_network_reply(qgis_reply)
log(f"http_status_code: {self.parsed_reply.http_status_code}")
log(f"qt_error: {self.parsed_reply.qt_error}")
self.request_parsed.emit()
else:
log(f"qgis_reply did not match the original reply id, ignoring...")


class BaseGeonodeClient(QtCore.QObject):
auth_config: str
base_url: str
network_fetcher_task: typing.Optional[NetworkFetcherTask]
network_fetcher_task: typing.Optional["NetworkFetcherTask"]
capabilities: typing.List[models.ApiClientCapability]

layer_list_received = QtCore.pyqtSignal(list, models.GeoNodePaginationInfo)
Expand Down Expand Up @@ -266,6 +133,7 @@ def get_layers(
search_params if search_params is not None else GeonodeApiSearchParameters()
)
self.network_fetcher_task = NetworkFetcherTask(
self,
QtNetwork.QNetworkRequest(self.get_layers_url_endpoint(params)),
request_payload=self.get_layers_request_payload(params),
authcfg=self.auth_config,
Expand All @@ -282,6 +150,7 @@ def get_layer_detail_from_brief_resource(

def get_layer_detail(self, id_: typing.Union[int, uuid.UUID]):
self.network_fetcher_task = NetworkFetcherTask(
self,
QtNetwork.QNetworkRequest(self.get_layer_detail_url_endpoint(id_)),
authcfg=self.auth_config,
)
Expand All @@ -293,7 +162,7 @@ def get_layer_styles(self, layer_id: int):
self.get_layer_styles_url_endpoint(layer_id)
)
self.network_fetcher_task = NetworkFetcherTask(
request, authcfg=self.auth_config
self, request, authcfg=self.auth_config
)
self.network_fetcher_task.request_finished.connect(self.handle_layer_style_list)
qgis.core.QgsApplication.taskManager().addTask(self.network_fetcher_task)
Expand All @@ -307,7 +176,9 @@ def get_layer_style(
style_details = [i for i in layer.styles if i.name == style_name][0]
style_url = style_details.sld_url
self.network_fetcher_task = NetworkFetcherTask(
QtNetwork.QNetworkRequest(QtCore.QUrl(style_url)), authcfg=self.auth_config
self,
QtNetwork.QNetworkRequest(QtCore.QUrl(style_url)),
authcfg=self.auth_config,
)
self.network_fetcher_task.request_finished.connect(
self.handle_layer_style_detail
Expand All @@ -319,6 +190,7 @@ def get_maps(self, search_params: GeonodeApiSearchParameters):
request_payload = self.get_maps_request_payload(search_params)
log(f"URL: {url.toString()}")
self.network_fetcher_task = NetworkFetcherTask(
self,
QtNetwork.QNetworkRequest(url),
request_payload=request_payload,
authcfg=self.auth_config,
Expand All @@ -327,3 +199,153 @@ def get_maps(self, search_params: GeonodeApiSearchParameters):
partial(self.handle_map_list, search_params)
)
qgis.core.QgsApplication.taskManager().addTask(self.network_fetcher_task)


class NetworkFetcherTask(qgis.core.QgsTask):
api_client: BaseGeonodeClient
authcfg: typing.Optional[str]
description: str
request: QtNetwork.QNetworkRequest
request_payload: typing.Optional[str]
reply_content: typing.Optional[QtCore.QByteArray]
parsed_reply: typing.Optional[ParsedNetworkReply]
redirect_policy: QtNetwork.QNetworkRequest.RedirectPolicy
_reply: typing.Optional[QtNetwork.QNetworkReply]

request_finished = QtCore.pyqtSignal()
request_parsed = QtCore.pyqtSignal()

def __init__(
self,
api_client: BaseGeonodeClient,
request: QtNetwork.QNetworkRequest,
request_payload: typing.Optional[str] = None,
authcfg: typing.Optional[str] = None,
description: typing.Optional[str] = "MyNetworkfetcherTask",
redirect_policy: typing.Optional[QtNetwork.QNetworkRequest.RedirectPolicy] = (
QtNetwork.QNetworkRequest.NoLessSafeRedirectPolicy
),
):
"""
Custom QgsTask that performs network requests
This class is able to perform both GET and POST HTTP requests.
It is needed because:
- QgsNetworkContentFetcherTask only performs GET requests
- QgsNetworkAcessManager.blockingPost() does not seem to handle redirects
correctly
Implementation is based on QgsNetworkContentFetcher. The run() method performs
a normal async request using QtNetworkAccessManager's get() or post() methods.
The resulting QNetworkReply instance has its `finished` signal be connected to
a custom handler. The request is executed in scope of a custom Qt event loop,
which blocks the current thread while the request is being processed.
"""

super().__init__(description)
self.api_client = api_client
self.authcfg = authcfg
self.request = request
self.request_payload = request_payload
self.reply_content = None
self.parsed_reply = None
self.redirect_policy = redirect_policy
self.network_access_manager = qgis.core.QgsNetworkAccessManager.instance()
self.network_access_manager.setRedirectPolicy(self.redirect_policy)
self.network_access_manager.finished.connect(self._request_done)
self._reply = None

def run(self):
if self.authcfg is not None:
auth_manager = qgis.core.QgsApplication.authManager()
auth_manager.updateNetworkRequest(self.request, self.authcfg)
with wait_for_signal(self.request_parsed) as loop_result:
if self.request_payload is None:
self._reply = self.network_access_manager.get(self.request)
else:
self._reply = self.network_access_manager.post(
self.request,
QtCore.QByteArray(self.request_payload.encode("utf-8")),
)
try:
if loop_result.result:
result = self.parsed_reply.qt_error is None
else:
result = False
self._reply.deleteLater()
self._reply = None
except AttributeError:
result = False
# self.network_access_manager.finished.disconnect(self._request_done)
# self.request_finished.emit()
return result

def finished(self, result: bool):
self.network_access_manager.finished.disconnect(self._request_done)
log(f"Inside finished. Result: {result}")
self.request_finished.emit()
if not result:
self.api_client.error_received.emit(
self.parsed_reply.qt_error,
self.parsed_reply.http_status_code,
self.parsed_reply.http_status_reason,
)

def _request_done(self, qgis_reply: qgis.core.QgsNetworkReplyContent):
log(f"requested_url: {qgis_reply.request().url().toString()}")
if self._reply is None:
log(
"Some other request was completed, probably authentication, "
"ignoring..."
)
elif reply_matches(qgis_reply, self._reply):
self.reply_content = self._reply.readAll()
self.parsed_reply = parse_network_reply(qgis_reply)
log(f"http_status_code: {self.parsed_reply.http_status_code}")
log(f"qt_error: {self.parsed_reply.qt_error}")
self.request_parsed.emit()
else:
log(f"qgis_reply did not match the original reply id, ignoring...")


@dataclasses.dataclass()
class EventLoopResult:
result: typing.Optional[bool]


@contextmanager
def wait_for_signal(
signal, timeout: int = 10000
) -> typing.ContextManager[EventLoopResult]:
"""Fire up a custom event loop and wait for the input signal to be emitted
This function allows running QT async code in a blocking fashion. It works by
spawning a Qt event loop. This custom loop has its `quit()` slot bound to the
input `signal`. The event loop is `exec_`'ed, thus blocking the current
thread until the the input `signal` is emitted.
"""

loop = QtCore.QEventLoop()
signal.connect(loop.quit)
loop_result = EventLoopResult(result=None)
yield loop_result
QtCore.QTimer.singleShot(timeout, partial(_forcibly_terminate_loop, loop))
log(f"About to start custom event loop...")
loop_result.result = not bool(loop.exec_())
log(f"Custom event loop ended, resuming...")


def _forcibly_terminate_loop(loop: QtCore.QEventLoop):
log("Forcibly ending event loop...")
loop.exit(1)


def reply_matches(
qgis_reply: qgis.core.QgsNetworkReplyContent, qt_reply: QtNetwork.QNetworkReply
) -> bool:
reply_id = int(qt_reply.property("requestId"))
return qgis_reply.requestId() == reply_id
Loading

0 comments on commit a335000

Please sign in to comment.