src/eric7/MicroPython/MipLocalInstaller.py

branch
mpy_network
changeset 9979
dbafba79461d
child 10439
21c28b0f9e41
equal deleted inserted replaced
9978:f878ae1e6d21 9979:dbafba79461d
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a MicroPython package installer for devices missing the onboard
8 'mip' package.
9 """
10
11 import json
12
13 from PyQt6.QtCore import QEventLoop, QObject, QUrl
14 from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
15
16 from eric7.EricNetwork.EricNetworkProxyFactory import proxyAuthenticationRequired
17
18 MicroPythonPackageIndex = "https://micropython.org/pi/v2"
19
20
21 class MipLocalInstaller(QObject):
22 """
23 Class implementing a MicroPython package installer ('mip' replacement).
24 """
25
26 def __init__(self, device, parent=None):
27 """
28 Constructor
29
30 @param device reference to the connected device
31 @type BaseDevice
32 @param parent reference to the parent object (defaults to None)
33 @type QObject (optional)
34 """
35 super().__init__(parent)
36
37 self.__device = device
38 self.__error = ""
39
40 self.__networkManager = QNetworkAccessManager(self)
41 self.__networkManager.proxyAuthenticationRequired.connect(
42 proxyAuthenticationRequired
43 )
44
45 self.__loop = QEventLoop()
46 self.__networkManager.finished.connect(self.__loop.quit)
47
48 def __rewriteUrl(self, url, branch=None):
49 """
50 Private method to rewrite the given URL in case of a Github URL.
51
52 @param url URL to be checked and potentially changed
53 @type str
54 @param branch branch name (defaults to None)
55 @type str (optional)
56 @return rewritten URL
57 @rtype str
58 """
59 if url.startswith("github:"):
60 urlList = url[7:].split("/")
61 if branch is None:
62 branch = "HEAD"
63 url = (
64 "https://raw.githubusercontent.com/"
65 + urlList[0]
66 + "/"
67 + urlList[1]
68 + "/"
69 + branch
70 + "/"
71 + "/".join(urlList[2:])
72 )
73
74 return url
75
76 def __getFile(self, fileUrl):
77 """
78 Private method to download the requested file.
79
80 @param fileUrl URL of the requested file
81 @type QUrl
82 @return package data or an error message and a success flag
83 @rtype tuple of (bytes or str, bool)
84 """
85 request = QNetworkRequest(fileUrl)
86 reply = self.__networkManager.get(request)
87 if not self.__loop.isRunning():
88 self.__loop.exec()
89 if reply.error() != QNetworkReply.NetworkError.NoError:
90 return reply.errorString(), False
91 else:
92 return bytes(reply.readAll()), True
93
94 def __installFile(self, fileUrl, targetDir, targetFile):
95 """
96 Private method to download a file and copy the data to the given target
97 directory.
98
99 @param fileUrl URL of the file to be downloaded and installed
100 @type str
101 @param targetDir target directory on the device
102 @type str
103 @param targetFile file name on the device
104 @type str
105 @return flag indicating success
106 @rtype bool
107 """
108 fileData, ok = self.__getFile(fileUrl)
109 if not ok:
110 self.__error = fileData
111 return False
112
113 try:
114 targetFilePath = "{0}/{1}".format(targetDir, targetFile)
115 self.__device.ensurePath(targetFilePath.rsplit("/", 1)[0])
116 self.__device.putData(targetFilePath, fileData)
117 except OSError as err:
118 self.__error = err
119 return False
120
121 return True
122
123 def __installJson(self, packageJson, version, mpy, target, index):
124 """
125 Private method to install a package and its dependencies as defined by the
126 package JSON file.
127
128 @param packageJson dictionary containing the package data
129 @type dict
130 @param version package version
131 @type str
132 @param mpy flag indicating to install as '.mpy' file
133 @type bool
134 @param target target directory on the device
135 @type str
136 @param index URL of the package index to be used
137 @type str
138 @return flag indicating success
139 @rtype bool
140 """
141 for targetFile, shortHash in packageJson.get("hashes", ()):
142 fileUrl = QUrl("{0}/file/{1}/{2}".format(index, shortHash[:2], shortHash))
143 if not self.__installFile(fileUrl, target, targetFile):
144 return False
145
146 for targetFile, url in packageJson.get("urls", ()):
147 if not self.__installFile(
148 self.__rewriteUrl(url, branch=version), target, targetFile
149 ):
150 return False
151
152 for dependency, dependencyVersion in packageJson.get("deps", ()):
153 self.installPackage(dependency, dependencyVersion, mpy, target=target)
154
155 return True
156
157 def installPackage(self, package, index=None, target=None, version=None, mpy=True):
158 """
159 Public method to install a MicroPython package.
160
161 @param package package name
162 @type str
163 @param index URL of the package index to be used (defaults to None)
164 @type str (optional)
165 @param target target directory on the device (defaults to None)
166 @type str (optional)
167 @param version package version (defaults to None)
168 @type str (optional)
169 @param mpy flag indicating to install as '.mpy' file (defaults to True)
170 @type bool (optional)
171 @return flag indicating success
172 @rtype bool
173 """
174 self.__error = ""
175
176 if not bool(index):
177 index = MicroPythonPackageIndex
178 index = index.rstrip("/")
179
180 if not target:
181 libPaths = self.__device.getLibPaths()
182 if libPaths and libPaths[0]:
183 target = libPaths[0]
184 else:
185 self.__error = self.tr(
186 "Unable to find 'lib' in sys.path. Please enter a target."
187 )
188 return False
189
190 if package.startswith(("http://", "https://", "github:")):
191 if package.endswith(".py") or package.endswith(".mpy"):
192 return self.__installFile(
193 self.__rewriteUrl(package, version),
194 target,
195 package.rsplit("/", 1)[-1],
196 )
197 else:
198 if not package.endswith(".json"):
199 if not package.endswith("/"):
200 package += "/"
201 package += "package.json"
202 else:
203 if not version:
204 version = "latest"
205
206 mpyVersion = "py"
207 if mpy and self.__device.getDeviceData("mpy_file_version") > 0:
208 mpyVersion = self.__device.getDeviceData("mpy_file_version")
209
210 packageJsonUrl = QUrl(
211 "{0}/package/{1}/{2}/{3}.json".format(
212 index, mpyVersion, package, version
213 )
214 )
215
216 jsonData, ok = self.__getFile(packageJsonUrl)
217 if not ok:
218 self.__error = jsonData
219 return False
220
221 try:
222 packageJson = json.loads(jsonData.decode("utf-8"))
223 except json.JSONDecodeError as err:
224 self.__error = str(err)
225 return False
226
227 ok = self.__installJson(packageJson, version, mpy, target, index)
228 if not ok:
229 self.__error += self.tr("\n\nPackage may be partially installed.")
230
231 return ok
232
233 def errorString(self):
234 """
235 Public method to get the last error as a string.
236
237 @return latest error
238 @rtype str
239 """
240 return self.__error

eric ide

mercurial