Beispiel #1
0
    def _onUploadSlotCompleted(
            self,
            reply: QNetworkReply,
            error: Optional["QNetworkReply.NetworkError"] = None) -> None:
        if error is not None:
            Logger.warning(str(error))
            self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
            self._job_done.set()
            return
        if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) >= 300:
            Logger.warning("Could not request backup upload: %s",
                           HttpRequestManager.readText(reply))
            self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
            self._job_done.set()
            return

        backup_upload_url = HttpRequestManager.readJSON(
            reply)["data"]["upload_url"]

        # Upload the backup to storage.
        HttpRequestManager.getInstance().put(
            backup_upload_url,
            data=self._backup_zip,
            callback=self._uploadFinishedCallback,
            error_callback=self._uploadFinishedCallback)
    def getBackups(self, changed: Callable[[List[Dict[str, Any]]],
                                           None]) -> None:
        def callback(
                reply: QNetworkReply,
                error: Optional["QNetworkReply.NetworkError"] = None) -> None:
            if error is not None:
                Logger.log("w", "Could not get backups: " + str(error))
                changed([])
                return

            backup_list_response = HttpRequestManager.readJSON(reply)
            if "data" not in backup_list_response:
                Logger.log(
                    "w",
                    "Could not get backups from remote, actual response body was: %s",
                    str(backup_list_response))
                changed([])  # empty list of backups
                return

            changed(backup_list_response["data"])

        HttpRequestManager.getInstance().get(self.BACKUP_URL,
                                             callback=callback,
                                             error_callback=callback,
                                             scope=self._json_cloud_scope)
Beispiel #3
0
 def abortRequest(self, request_id: str) -> None:
     """Aborts a single request"""
     if request_id in self._ongoing_requests and self._ongoing_requests[
             request_id]:
         HttpRequestManager.getInstance().abortRequest(
             self._ongoing_requests[request_id])
         self._ongoing_requests[request_id] = None
Beispiel #4
0
    def unsunscribeUserFromPackage(self, package_id: str) -> None:
        """Unsubscribe the user (if logged in) from the package

         :param package_id: the package identification string
         """
        if self._account.isLoggedIn:
            HttpRequestManager.getInstance().delete(
                url=f"{USER_PACKAGES_URL}/{package_id}", scope=self._scope)
Beispiel #5
0
    def _uploadFinishedCallback(self,
                                reply: QNetworkReply,
                                error: QNetworkReply.NetworkError = None):
        if not HttpRequestManager.replyIndicatesSuccess(reply, error):
            Logger.log("w", "Could not upload backup file: %s",
                       HttpRequestManager.readText(reply))
            self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE

        self._job_done.set()
Beispiel #6
0
    def _subscribe(self, package_id: str) -> None:
        """You probably don't want to use this directly. All installed packages will be automatically subscribed."""

        Logger.debug("Subscribing to {}", package_id)
        data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (
            package_id, CloudApiModel.sdk_version)
        HttpRequestManager.getInstance().put(
            url=CloudApiModel.api_url_user_packages,
            data=data.encode(),
            scope=self._scope)
Beispiel #7
0
    def run(self) -> None:

        url = self._backup.get("download_url")
        assert url is not None

        HttpRequestManager.getInstance().get(
            url = url,
            callback = self._onRestoreRequestCompleted,
            error_callback = self._onRestoreRequestCompleted
        )

        self._job_done.wait()  # A job is considered finished when the run function completes
Beispiel #8
0
    def download(self,
                 package_id: str,
                 url: str,
                 update: bool = False) -> None:
        """Initiate the download request

        :param package_id: the package identification string
        :param url: the URL from which the package needs to be obtained
        :param update: A flag if this is download request is an update process
        """

        if url == "":
            url = f"{PACKAGES_URL}/{package_id}/download"

        def downloadFinished(reply: "QNetworkReply") -> None:
            self._downloadFinished(package_id, reply, update)

        def downloadError(reply: "QNetworkReply",
                          error: "QNetworkReply.NetworkError") -> None:
            self._downloadError(package_id, update, reply, error)

        self._ongoing_requests[
            "download_package"] = HttpRequestManager.getInstance().get(
                url,
                scope=self._scope,
                callback=downloadFinished,
                error_callback=downloadError)
def test_getBasicAuthSuccess() -> None:
    time.sleep(0.5)
    app = QCoreApplication([])
    http_request_manager = HttpRequestManager.getInstance()

    cbo = mock.Mock()

    def callback(*args, **kwargs):
        cbo.callback(*args, **kwargs)
        # quit now so we don't need to wait
        http_request_manager.callLater(0, app.quit)

    def error_callback(*args, **kwargs):
        cbo.callback(*args, **kwargs)
        # quit now so we don't need to wait
        http_request_manager.callLater(0, app.quit)

    request_data = http_request_manager.get(
        url="http://localhost:8080/auth",
        headers_dict={"Authorization": "Basic dXNlcjp1c2Vy"},
        callback=callback,
        error_callback=error_callback)
    # Make sure that if something goes wrong, we quit after 10 seconds
    http_request_manager.callLater(10.0, app.quit)

    app.exec()
    http_request_manager.cleanup()  # Remove all unscheduled events

    cbo.callback.assert_called_once_with(request_data.reply)
    cbo.error_callback.assert_not_called()
Beispiel #10
0
    def _parseResponse(self, reply: "QNetworkReply") -> None:
        """
        Parse the response from the package list API request which can update.

        :param reply: A reply containing information about a number of packages.
        """
        response_data = HttpRequestManager.readJSON(reply)
        if "data" not in response_data:
            Logger.error(
                f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}"
            )
            return
        if len(response_data["data"]) == 0:
            return

        packages = response_data["data"]
        for package in packages:
            self._package_manager.addAvailablePackageVersion(
                package["package_id"], Version(package["package_version"]))
            package_model = self.getPackageModel(package["package_id"])
            if package_model:
                # Also make sure that the local list knows where to get an update
                package_model.setDownloadUrl(package["download_url"])

        self._ongoing_requests["check_updates"] = None
def test_getFail404() -> None:
    time.sleep(0.5)
    app = QCoreApplication([])
    http_request_manager = HttpRequestManager.getInstance()

    cbo = mock.Mock()

    def callback(*args, **kwargs):
        cbo.callback(*args, **kwargs)
        # quit now so we don't need to wait
        http_request_manager.callLater(0, app.quit)

    def error_callback(*args, **kwargs):
        cbo.error_callback(*args, **kwargs)
        # quit now so we don't need to wait
        http_request_manager.callLater(0, app.quit)

    request_data = http_request_manager.get(
        url="http://localhost:8080/do_not_exist",
        callback=callback,
        error_callback=error_callback)
    # Make sure that if something goes wrong, we quit after 10 seconds
    http_request_manager.callLater(10.0, app.quit)

    app.exec()
    http_request_manager.cleanup()  # Remove all unscheduled events

    cbo.error_callback.assert_called_once_with(
        request_data.reply, QNetworkReply.ContentNotFoundError)
    cbo.callback.assert_not_called()
Beispiel #12
0
    def _parseResponse(self, reply: "QNetworkReply") -> None:
        """
        Parse the response from the package list API request.

        This converts that response into PackageModels, and triggers the ListModel to update.
        :param reply: A reply containing information about a number of packages.
        """
        response_data = HttpRequestManager.readJSON(reply)
        if "data" not in response_data or "links" not in response_data:
            Logger.error(f"Could not interpret the server's response. Missing 'data' or 'links' from response data. Keys in response: {response_data.keys()}")
            self.setErrorMessage(catalog.i18nc("@info:error", "Could not interpret the server's response."))
            return

        for package_data in response_data["data"]:
            try:
                package = PackageModel(package_data, parent = self)
                self._connectManageButtonSignals(package)
                self.appendItem({"package": package})  # Add it to this list model.
            except RuntimeError:
                # Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling
                # between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object
                # was deleted when it was still parsing the response
                continue

        self._request_url = response_data["links"].get("next", "")  # Use empty string to signify that there is no next page.
        self._ongoing_requests["get_packages"] = None
        self.setIsLoading(False)
        self.setHasMore(self._request_url != "")
Beispiel #13
0
    def __init__(self, parent: Optional["QObject"] = None) -> None:
        super().__init__(parent)

        manager = HttpRequestManager.getInstance()
        self._is_internet_reachable = manager.isInternetReachable  # type: bool
        manager.internetReachableChanged.connect(
            self._onInternetReachableChanged)
Beispiel #14
0
    def download(self, model: SubscribedPackagesModel) -> None:
        if self._started:
            Logger.error("Download already started. Create a new %s instead", self.__class__.__name__)
            return

        manager = HttpRequestManager.getInstance()
        for item in model.items:
            package_id = item["package_id"]

            def finishedCallback(reply: QNetworkReply, pid = package_id) -> None:
                self._onFinished(pid, reply)

            def progressCallback(rx: int, rt: int, pid = package_id) -> None:
                self._onProgress(pid, rx, rt)

            def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None:
                self._onError(pid)

            request_data = manager.get(
                item["download_url"],
                callback = finishedCallback,
                download_progress_callback = progressCallback,
                error_callback = errorCallback,
                scope = self._scope)

            self._progress[package_id] = {
                "received": 0,
                "total": 1,  # make sure this is not considered done yet. Also divByZero-safe
                "file_written": None,
                "request_data": request_data,
                "package_model": item
            }

        self._started = True
        self._progress_message.show()
def test_getTimeout() -> None:
    time.sleep(0.5)
    app = QCoreApplication([])
    http_request_manager = HttpRequestManager.getInstance()

    cbo = mock.Mock()

    def error_callback(*args, **kwargs):
        cbo.error_callback(*args, **kwargs)
        # quit now so we don't need to wait
        http_request_manager.callLater(0, app.quit)

    request_data = http_request_manager.get(
        url="http://localhost:8080/timeout",
        error_callback=error_callback,
        timeout=4)
    # Make sure that if something goes wrong, we quit after 10 seconds
    http_request_manager.callLater(10.0, app.quit)

    app.exec()
    http_request_manager.cleanup()  # Remove all unscheduled events

    cbo.error_callback.assert_called_once_with(
        request_data.reply, QNetworkReply.OperationCanceledError)
    assert request_data.is_aborted_due_to_timeout
Beispiel #16
0
    def _statusCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None):
        url = reply.request().url().toString()
        prev_statuses = self._statuses.copy()
        self._statuses[url] = HttpRequestManager.replyIndicatesSuccess(reply, error)

        if any(self._statuses.values()) != any(prev_statuses.values()):
            self.internetReachableChanged.emit()
Beispiel #17
0
    def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
        if HttpRequestManager.safeHttpStatus(reply) >= 300:
            replyText = HttpRequestManager.readText(reply)
            Logger.warning("Could not request backup upload: %s", replyText)
            self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE

            if HttpRequestManager.safeHttpStatus(reply) == 400:
                errors = json.loads(replyText)["errors"]
                if "moreThanMaximum" in [error["code"] for error in errors if error["meta"] and error["meta"]["field_name"] == "backup_size"]:
                    self.backup_upload_error_message = catalog.i18nc("@error:file_size", "The backup exceeds the maximum file size.")

            self._job_done.set()
            return

        if error is not None:
            Logger.warning("Could not request backup upload: %s", HttpRequestManager.qt_network_error_name(error))
            self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
            self._job_done.set()
            return

        backup_upload_url = HttpRequestManager.readJSON(reply)["data"]["upload_url"]

        # Upload the backup to storage.
        HttpRequestManager.getInstance().put(
            backup_upload_url,
            data=self._backup_zip,
            callback=self._uploadFinishedCallback,
            error_callback=self._uploadFinishedCallback
        )
Beispiel #18
0
 def __init__(self, app: CuraApplication,
              on_error: Callable[[List[CloudError]], None]) -> None:
     super().__init__()
     self._app = app
     self._account = app.getCuraAPI().account
     self._scope = JsonDecoratorScope(UltimakerCloudScope(app))
     self._http = HttpRequestManager.getInstance()
     self._on_error = on_error
     self._upload = None  # type: Optional[ToolPathUploader]
    def deleteBackup(self, backup_id: str, finished_callable: Callable[[bool],
                                                                       None]):
        def finishedCallback(
                reply: QNetworkReply,
                ca: Callable[[bool], None] = finished_callable) -> None:
            self._onDeleteRequestCompleted(reply, ca)

        def errorCallback(
                reply: QNetworkReply,
                error: QNetworkReply.NetworkError,
                ca: Callable[[bool], None] = finished_callable) -> None:
            self._onDeleteRequestCompleted(reply, ca, error)

        HttpRequestManager.getInstance().delete(url="{}/{}".format(
            self.BACKUP_URL, backup_id),
                                                callback=finishedCallback,
                                                error_callback=errorCallback,
                                                scope=self._json_cloud_scope)
Beispiel #20
0
    def subscribeUserToPackage(self, package_id: str,
                               sdk_version: str) -> None:
        """Subscribe the user (if logged in) to the package for a given SDK

         :param package_id: the package identification string
         :param sdk_version: the SDK version
         """
        if self._account.isLoggedIn:
            HttpRequestManager.getInstance().put(url=USER_PACKAGES_URL,
                                                 data=json.dumps({
                                                     "data": {
                                                         "package_id":
                                                         package_id,
                                                         "sdk_version":
                                                         sdk_version
                                                     }
                                                 }).encode(),
                                                 scope=self._scope)
Beispiel #21
0
    def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None:
        """Request a backup upload slot from the API.

        :param backup_metadata: A dict containing some meta data about the backup.
        :param backup_size: The size of the backup file in bytes.
        """

        payload = json.dumps({"data": {"backup_size": backup_size,
                                       "metadata": backup_metadata
                                       }
                              }).encode()

        HttpRequestManager.getInstance().put(
            self._api_backup_url,
            data = payload,
            callback = self._onUploadSlotCompleted,
            error_callback = self._onUploadSlotCompleted,
            scope = self._json_cloud_scope)
    def _openSelectedFile(self, temp_dir: str, project_name: str, file_name: str, download_url: str) -> None:
        """ Downloads, then opens, the single specified file.

        :param temp_dir: The already created temporary directory where the files will be stored.
        :param project_name: Name of the project the file belongs to (used for error reporting).
        :param file_name: Name of the file to be downloaded and opened (used for error reporting).
        :param download_url: This url will be downloaded, then the downloaded file will be opened in Cura.
        """
        if not download_url:
            Logger.log("e", "No download url for file '{}'".format(file_name))
            return

        progress_message = Message(text = "{0}/{1}".format(project_name, file_name), dismissable = False, lifetime = 0,
                                   progress = 0, title = "Downloading...")
        progress_message.setProgress(0)
        progress_message.show()

        def progressCallback(rx: int, rt: int) -> None:
            progress_message.setProgress(math.floor(rx * 100.0 / rt))

        def finishedCallback(reply: QNetworkReply) -> None:
            progress_message.hide()
            try:
                with open(os.path.join(temp_dir, file_name), "wb+") as temp_file:
                    bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
                    while bytes_read:
                        temp_file.write(bytes_read)
                        bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
                        CuraApplication.getInstance().processEvents()
                    temp_file_name = temp_file.name
            except IOError as ex:
                Logger.logException("e", "Can't write Digital Library file {0}/{1} download to temp-directory {2}.",
                                    ex, project_name, file_name, temp_dir)
                Message(
                        text = "Failed to write to temporary file for '{}'.".format(file_name),
                        title = "File-system error",
                        lifetime = 10
                ).show()
                return

            CuraApplication.getInstance().readLocalFile(
                    QUrl.fromLocalFile(temp_file_name), add_to_recent_files = False)

        def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, p = project_name,
                          f = file_name) -> None:
            progress_message.hide()
            Logger.error("An error {0} {1} occurred while downloading {2}/{3}".format(str(error), str(reply), p, f))
            Message(
                    text = "Failed Digital Library download for '{}'.".format(f),
                    title = "Network error {}".format(error),
                    lifetime = 10
            ).show()

        download_manager = HttpRequestManager.getInstance()
        download_manager.get(download_url, callback = finishedCallback, download_progress_callback = progressCallback,
                             error_callback = errorCallback, scope = UltimakerCloudScope(CuraApplication.getInstance()))
Beispiel #23
0
    def checkForUpdates(self, packages: List[Dict[str, Any]]) -> None:
        installed_packages = "&".join([
            f"installed_packages={package['package_id']}:{package['package_version']}"
            for package in packages
        ])
        request_url = f"{PACKAGE_UPDATES_URL}?{installed_packages}"

        self._ongoing_requests[
            "check_updates"] = HttpRequestManager.getInstance().get(
                request_url, scope=self._scope, callback=self._parseResponse)
Beispiel #24
0
    def __init__(self, tray_icon_name: str = None, **kwargs) -> None:
        plugin_path = ""
        if sys.platform == "win32":
            if hasattr(sys, "frozen"):
                plugin_path = os.path.join(
                    os.path.dirname(os.path.abspath(sys.executable)), "PyQt5",
                    "plugins")
                Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
                QCoreApplication.addLibraryPath(plugin_path)
            else:
                import site
                for sitepackage_dir in site.getsitepackages():
                    QCoreApplication.addLibraryPath(
                        os.path.join(sitepackage_dir, "PyQt5", "plugins"))
        elif sys.platform == "darwin":
            plugin_path = os.path.join(self.getInstallPrefix(), "Resources",
                                       "plugins")

        if plugin_path:
            Logger.log("i", "Adding QT5 plugin path: %s", plugin_path)
            QCoreApplication.addLibraryPath(plugin_path)

        # use Qt Quick Scene Graph "basic" render loop
        os.environ["QSG_RENDER_LOOP"] = "basic"

        super().__init__(sys.argv, **kwargs)  # type: ignore

        self._qml_import_paths = []  #type: List[str]
        self._main_qml = "main.qml"  #type: str
        self._qml_engine = None  #type: Optional[QQmlApplicationEngine]
        self._main_window = None  #type: Optional[MainWindow]
        self._tray_icon_name = tray_icon_name  #type: Optional[str]
        self._tray_icon = None  #type: Optional[str]
        self._tray_icon_widget = None  #type: Optional[QSystemTrayIcon]
        self._theme = None  #type: Optional[Theme]
        self._renderer = None  #type: Optional[QtRenderer]

        self._job_queue = None  #type: Optional[JobQueue]
        self._version_upgrade_manager = None  #type: Optional[VersionUpgradeManager]

        self._is_shutting_down = False  #type: bool

        self._recent_files = []  #type: List[QUrl]

        self._configuration_error_message = None  #type: Optional[ConfigurationErrorMessage]

        self._http_network_request_manager = HttpRequestManager(parent=self)

        #Metadata required for the file dialogues.
        self.setOrganizationDomain("https://ultimaker.com/")
        self.setOrganizationName("Ultimaker B.V.")
Beispiel #25
0
    def updatePackages(self) -> None:
        """
        Make a request for the first paginated page of packages.

        When the request is done, the list will get updated with the new package models.
        """
        self.setErrorMessage("")  # Clear any previous errors.
        self.setIsLoading(True)

        self._ongoing_requests["get_packages"] = HttpRequestManager.getInstance().get(
            self._request_url,
            scope = self._scope,
            callback = self._parseResponse,
            error_callback = self._onError
        )
Beispiel #26
0
    def __init__(self, app: CuraApplication,
                 on_error: Callable[[List[CloudError]], None]) -> None:
        """Initializes a new cloud API client.

        :param app:
        :param account: The user's account object
        :param on_error: The callback to be called whenever we receive errors from the server.
        """
        super().__init__()
        self._app = app
        self._account = app.getCuraAPI().account
        self._scope = JsonDecoratorScope(UltimakerCloudScope(app))
        self._http = HttpRequestManager.getInstance()
        self._on_error = on_error
        self._upload = None  # type: Optional[ToolPathUploader]
Beispiel #27
0
    def __init__(self, parent: Optional["QObject"] = None):
        super().__init__(parent)

        self._http = HttpRequestManager.getInstance()
        self._statuses = {
            self.ULTIMAKER_CLOUD_STATUS_URL: True,
            "http://example.com": True
        }

        # Create a timer for automatic updates
        self._update_timer = QTimer()
        self._update_timer.setInterval(int(self.UPDATE_INTERVAL * 1000))
        # The timer is restarted automatically
        self._update_timer.setSingleShot(False)
        self._update_timer.timeout.connect(self._update)
        self._update_timer.start()
        def callback(
                reply: QNetworkReply,
                error: Optional["QNetworkReply.NetworkError"] = None) -> None:
            if error is not None:
                Logger.log("w", "Could not get backups: " + str(error))
                changed([])
                return

            backup_list_response = HttpRequestManager.readJSON(reply)
            if "data" not in backup_list_response:
                Logger.log(
                    "w",
                    "Could not get backups from remote, actual response body was: %s",
                    str(backup_list_response))
                changed([])  # empty list of backups
                return

            changed(backup_list_response["data"])
Beispiel #29
0
    def _onUploadSlotCompleted(
            self,
            reply: QNetworkReply,
            error: Optional["QNetworkReply.NetworkError"] = None) -> None:
        if HttpRequestManager.safeHttpStatus(reply) >= 300:
            replyText = HttpRequestManager.readText(reply)
            Logger.warning("Could not request backup upload: %s", replyText)
            self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE

            if HttpRequestManager.safeHttpStatus(reply) == 400:
                errors = json.loads(replyText)["errors"]
                if "moreThanMaximum" in [
                        error["code"] for error in errors if error["meta"]
                        and error["meta"]["field_name"] == "backup_size"
                ]:
                    if self._backup_zip is None:  # will never happen; keep mypy happy
                        zip_error = "backup is None."
                    else:
                        zip_error = "{} exceeds max size.".format(
                            str(len(self._backup_zip)))
                    sentry_sdk.capture_message(
                        "backup failed: {}".format(zip_error), level="warning")
                    self.backup_upload_error_message = catalog.i18nc(
                        "@error:file_size",
                        "The backup exceeds the maximum file size.")
                    from sentry_sdk import capture_message

            self._job_done.set()
            return

        if error is not None:
            Logger.warning("Could not request backup upload: %s",
                           HttpRequestManager.qt_network_error_name(error))
            self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
            self._job_done.set()
            return

        backup_upload_url = HttpRequestManager.readJSON(
            reply)["data"]["upload_url"]

        # Upload the backup to storage.
        HttpRequestManager.getInstance().put(
            backup_upload_url,
            data=self._backup_zip,
            callback=self._uploadFinishedCallback,
            error_callback=self._uploadFinishedCallback)
Beispiel #30
0
    def checkNewVersion(self, silent=False, display_same_version=True) -> None:
        """Connect with software.ultimaker.com, load latest.json and check version info.

        If the version info is higher then the current version, spawn a message to
        allow the user to download it.

        :param silent: Suppresses messages other than "new version found"
        messages. This is used when checking for a new version at startup.
        :param display_same_version: Whether to display the same update message
        twice (True) or suppress the update message if the user has already seen
        the update for a particular version. When manually checking for updates,
        the user wants to display the update even if he's already seen it.
        """
        http_manager = HttpRequestManager.getInstance()
        Logger.log("i", "Checking for new version")
        http_manager.get(self.url,
                         callback=lambda reply: self._onRequestCompleted(
                             reply, silent, display_same_version))
        self._download_url = None