src/eric7/MicroPython/MipLocalInstaller.py

branch
mpy_network
changeset 9979
dbafba79461d
child 10439
21c28b0f9e41
diff -r f878ae1e6d21 -r dbafba79461d src/eric7/MicroPython/MipLocalInstaller.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/MicroPython/MipLocalInstaller.py	Sat Apr 15 18:22:09 2023 +0200
@@ -0,0 +1,240 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a MicroPython package installer for devices missing the onboard
+'mip' package.
+"""
+
+import json
+
+from PyQt6.QtCore import QEventLoop, QObject, QUrl
+from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
+
+from eric7.EricNetwork.EricNetworkProxyFactory import proxyAuthenticationRequired
+
+MicroPythonPackageIndex = "https://micropython.org/pi/v2"
+
+
+class MipLocalInstaller(QObject):
+    """
+    Class implementing a MicroPython package installer ('mip' replacement).
+    """
+
+    def __init__(self, device, parent=None):
+        """
+        Constructor
+
+        @param device reference to the connected device
+        @type BaseDevice
+        @param parent reference to the parent object (defaults to None)
+        @type QObject (optional)
+        """
+        super().__init__(parent)
+
+        self.__device = device
+        self.__error = ""
+
+        self.__networkManager = QNetworkAccessManager(self)
+        self.__networkManager.proxyAuthenticationRequired.connect(
+            proxyAuthenticationRequired
+        )
+
+        self.__loop = QEventLoop()
+        self.__networkManager.finished.connect(self.__loop.quit)
+
+    def __rewriteUrl(self, url, branch=None):
+        """
+        Private method to rewrite the given URL in case of a Github URL.
+
+        @param url URL to be checked and potentially changed
+        @type str
+        @param branch branch name (defaults to None)
+        @type str (optional)
+        @return rewritten URL
+        @rtype str
+        """
+        if url.startswith("github:"):
+            urlList = url[7:].split("/")
+            if branch is None:
+                branch = "HEAD"
+            url = (
+                "https://raw.githubusercontent.com/"
+                + urlList[0]
+                + "/"
+                + urlList[1]
+                + "/"
+                + branch
+                + "/"
+                + "/".join(urlList[2:])
+            )
+
+        return url
+
+    def __getFile(self, fileUrl):
+        """
+        Private method to download the requested file.
+
+        @param fileUrl URL of the requested file
+        @type QUrl
+        @return package data or an error message and a success flag
+        @rtype tuple of (bytes or str, bool)
+        """
+        request = QNetworkRequest(fileUrl)
+        reply = self.__networkManager.get(request)
+        if not self.__loop.isRunning():
+            self.__loop.exec()
+        if reply.error() != QNetworkReply.NetworkError.NoError:
+            return reply.errorString(), False
+        else:
+            return bytes(reply.readAll()), True
+
+    def __installFile(self, fileUrl, targetDir, targetFile):
+        """
+        Private method to download a file and copy the data to the given target
+        directory.
+
+        @param fileUrl URL of the file to be downloaded and installed
+        @type str
+        @param targetDir target directory on the device
+        @type str
+        @param targetFile file name on the device
+        @type str
+        @return flag indicating success
+        @rtype  bool
+        """
+        fileData, ok = self.__getFile(fileUrl)
+        if not ok:
+            self.__error = fileData
+            return False
+
+        try:
+            targetFilePath = "{0}/{1}".format(targetDir, targetFile)
+            self.__device.ensurePath(targetFilePath.rsplit("/", 1)[0])
+            self.__device.putData(targetFilePath, fileData)
+        except OSError as err:
+            self.__error = err
+            return False
+
+        return True
+
+    def __installJson(self, packageJson, version, mpy, target, index):
+        """
+        Private method to install a package and its dependencies as defined by the
+        package JSON file.
+
+        @param packageJson dictionary containing the package data
+        @type dict
+        @param version package version
+        @type str
+        @param mpy flag indicating to install as '.mpy' file
+        @type bool
+        @param target target directory on the device
+        @type str
+        @param index URL of the package index to be used
+        @type str
+        @return flag indicating success
+        @rtype  bool
+        """
+        for targetFile, shortHash in packageJson.get("hashes", ()):
+            fileUrl = QUrl("{0}/file/{1}/{2}".format(index, shortHash[:2], shortHash))
+            if not self.__installFile(fileUrl, target, targetFile):
+                return False
+
+        for targetFile, url in packageJson.get("urls", ()):
+            if not self.__installFile(
+                self.__rewriteUrl(url, branch=version), target, targetFile
+            ):
+                return False
+
+        for dependency, dependencyVersion in packageJson.get("deps", ()):
+            self.installPackage(dependency, dependencyVersion, mpy, target=target)
+
+        return True
+
+    def installPackage(self, package, index=None, target=None, version=None, mpy=True):
+        """
+        Public method to install a MicroPython package.
+
+        @param package package name
+        @type str
+        @param index URL of the package index to be used (defaults to None)
+        @type str (optional)
+        @param target target directory on the device (defaults to None)
+        @type str (optional)
+        @param version package version (defaults to None)
+        @type str (optional)
+        @param mpy flag indicating to install as '.mpy' file (defaults to True)
+        @type bool (optional)
+        @return flag indicating success
+        @rtype  bool
+        """
+        self.__error = ""
+
+        if not bool(index):
+            index = MicroPythonPackageIndex
+        index = index.rstrip("/")
+
+        if not target:
+            libPaths = self.__device.getLibPaths()
+            if libPaths and libPaths[0]:
+                target = libPaths[0]
+            else:
+                self.__error = self.tr(
+                    "Unable to find 'lib' in sys.path. Please enter a target."
+                )
+                return False
+
+        if package.startswith(("http://", "https://", "github:")):
+            if package.endswith(".py") or package.endswith(".mpy"):
+                return self.__installFile(
+                    self.__rewriteUrl(package, version),
+                    target,
+                    package.rsplit("/", 1)[-1],
+                )
+            else:
+                if not package.endswith(".json"):
+                    if not package.endswith("/"):
+                        package += "/"
+                    package += "package.json"
+        else:
+            if not version:
+                version = "latest"
+
+            mpyVersion = "py"
+            if mpy and self.__device.getDeviceData("mpy_file_version") > 0:
+                mpyVersion = self.__device.getDeviceData("mpy_file_version")
+
+            packageJsonUrl = QUrl(
+                "{0}/package/{1}/{2}/{3}.json".format(
+                    index, mpyVersion, package, version
+                )
+            )
+
+        jsonData, ok = self.__getFile(packageJsonUrl)
+        if not ok:
+            self.__error = jsonData
+            return False
+
+        try:
+            packageJson = json.loads(jsonData.decode("utf-8"))
+        except json.JSONDecodeError as err:
+            self.__error = str(err)
+            return False
+
+        ok = self.__installJson(packageJson, version, mpy, target, index)
+        if not ok:
+            self.__error += self.tr("\n\nPackage may be partially installed.")
+
+        return ok
+
+    def errorString(self):
+        """
+        Public method to get the last error as a string.
+
+        @return latest error
+        @rtype str
+        """
+        return self.__error

eric ide

mercurial