Addon Manager: Create NetworkManager class

To enable single-login authenticated proxy use, and simplified multi-threaded
network accesses, this commit adds a new wrapper around a QNetworkAccessManager
and includes a global instantiation of the class intended to exist for the
lifetime of the program. This instance can be used to enqueue any number of
network requests, which the manager will send out to the networking subsystem
in an appropriate manner.
This commit is contained in:
Chris Hennes
2022-01-14 11:52:31 -06:00
parent cad0d01883
commit e9d6a2c4a4
8 changed files with 1055 additions and 660 deletions

View File

@@ -35,10 +35,13 @@ import time
import subprocess
import sys
import platform
import itertools
from datetime import datetime
from typing import Union, List
from typing import Union, List, Dict
from enum import Enum, auto
from PySide2 import QtCore, QtNetwork
from PySide2 import QtCore
import FreeCAD
@@ -47,12 +50,9 @@ if FreeCAD.GuiUp:
import addonmanager_utilities as utils
from addonmanager_macro import Macro
from addonmanager_metadata import (
MetadataDownloadWorker,
MetadataTxtDownloadWorker,
RequirementsTxtDownloadWorker,
)
from AddonManagerRepo import AddonManagerRepo
from NetworkManager import AM_NETWORK_MANAGER
translate = FreeCAD.Qt.translate
@@ -119,18 +119,7 @@ class ConnectionChecker(QtCore.QThread):
translate("AddonsInstaller", "Checking network connection...\n")
)
url = "https://api.github.com/zen"
request = utils.urlopen(url)
if QtCore.QThread.currentThread().isInterruptionRequested():
return
if not request:
self.failure.emit(
translate(
"AddonsInstaller",
"Unable to connect to GitHub: check your internet connection and proxy settings and try again.",
)
)
return
result = request.read()
result = AM_NETWORK_MANAGER.blocking_get(url)
if QtCore.QThread.currentThread().isInterruptionRequested():
return
if not result:
@@ -142,7 +131,7 @@ class ConnectionChecker(QtCore.QThread):
)
return
result = result.decode("utf8")
result = result.data().decode("utf8")
FreeCAD.Console.PrintLog(f"GitHub's zen message response: {result}\n")
self.success.emit()
@@ -164,12 +153,11 @@ class UpdateWorker(QtCore.QThread):
# update info lists
global obsolete, macros_reject_list, mod_reject_list, py2only
u = utils.urlopen(
p = AM_NETWORK_MANAGER.blocking_get(
"https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/addonflags.json"
)
if u:
p = u.read()
u.close()
if p:
p = p.data().decode("utf8")
j = json.loads(p)
if "obsolete" in j and "Mod" in j["obsolete"]:
obsolete = j["obsolete"]["Mod"]
@@ -266,15 +254,12 @@ class UpdateWorker(QtCore.QThread):
self.addon_repo.emit(repo)
# querying official addons
u = utils.urlopen(
p = AM_NETWORK_MANAGER.blocking_get(
"https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/.gitmodules"
)
if not u:
if not p:
return
p = u.read()
if isinstance(p, bytes):
p = p.decode("utf-8")
u.close()
p = p.data().decode("utf8")
p = re.findall(
(
r'(?m)\[submodule\s*"(?P<name>.*)"\]\s*'
@@ -642,8 +627,8 @@ class FillMacroListWorker(QtCore.QThread):
Reads only the page https://wiki.freecad.org/Macros_recipes
"""
u = utils.urlopen("https://wiki.freecad.org/Macros_recipes")
if not u:
p = AM_NETWORK_MANAGER.blocking_get("https://wiki.freecad.org/Macros_recipes")
if not p:
FreeCAD.Console.PrintWarning(
translate(
"AddonsInstaller",
@@ -652,10 +637,7 @@ class FillMacroListWorker(QtCore.QThread):
+ "\n"
)
return
p = u.read()
u.close()
if isinstance(p, bytes):
p = p.decode("utf-8")
p = p.data().decode("utf8")
macros = re.findall('title="(Macro.*?)"', p)
macros = [mac for mac in macros if ("translated" not in mac)]
macro_names = []
@@ -835,18 +817,13 @@ class ShowWorker(QtCore.QThread):
readmeurl = utils.get_readme_html_url(self.repo)
if not readmeurl:
FreeCAD.Console.PrintLog(f"README not found for {url}\n")
u = utils.urlopen(readmeurl)
if not u:
p = AM_NETWORK_MANAGER.blocking_get(readmeurl)
if not p:
FreeCAD.Console.PrintLog(f"Debug: README not found at {readmeurl}\n")
u = utils.urlopen(readmeurl)
if u:
p = u.read()
if isinstance(p, bytes):
p = p.decode("utf-8")
u.close()
readme = re.findall(regex, p, flags=re.MULTILINE | re.DOTALL)
if readme:
desc = readme[0]
p = p.data().decode("utf8")
readme = re.findall(regex, p, flags=re.MULTILINE | re.DOTALL)
if readme:
desc = readme[0]
else:
FreeCAD.Console.PrintLog(f"Debug: README not found at {readmeurl}\n")
else:
@@ -854,12 +831,9 @@ class ShowWorker(QtCore.QThread):
readmeurl = utils.get_readme_url(self.repo)
if not readmeurl:
FreeCAD.Console.PrintLog(f"Debug: README not found for {url}\n")
u = utils.urlopen(readmeurl)
if u:
p = u.read()
if isinstance(p, bytes):
p = p.decode("utf-8")
u.close()
p = AM_NETWORK_MANAGER.blocking_get(readmeurl)
if p:
p = p.data().decode("utf8")
desc = utils.fix_relative_links(p, readmeurl.rsplit("/README.md")[0])
if not NOMARKDOWN and have_markdown:
desc = markdown.markdown(desc, extensions=["md_in_html"])
@@ -877,25 +851,22 @@ class ShowWorker(QtCore.QThread):
desc = message
else:
FreeCAD.Console.PrintLog("Debug: README not found at {readmeurl}\n")
if desc == "":
# fall back to the description text
u = utils.urlopen(url)
if not u:
if desc == "":
# fall back to the description text
p = AM_NETWORK_MANAGER.blocking_get(url)
if not p:
return
p = p.data().decode("utf8")
descregex = utils.get_desc_regex(self.repo)
if descregex:
desc = re.findall(descregex, p)
if desc:
desc = desc[0]
if not desc:
desc = "Unable to retrieve addon description"
self.repo.description = desc
if QtCore.QThread.currentThread().isInterruptionRequested():
return
p = u.read()
if isinstance(p, bytes):
p = p.decode("utf-8")
u.close()
descregex = utils.get_desc_regex(self.repo)
if descregex:
desc = re.findall(descregex, p)
if desc:
desc = desc[0]
if not desc:
desc = "Unable to retrieve addon description"
self.repo.description = desc
if QtCore.QThread.currentThread().isInterruptionRequested():
return
message = desc
if self.repo.update_status == AddonManagerRepo.UpdateStatus.UNCHECKED:
# Addon is installed but we haven't checked it yet, so let's check if it has an update
@@ -920,7 +891,7 @@ class ShowWorker(QtCore.QThread):
AddonManagerRepo.UpdateStatus.NO_UPDATE_AVAILABLE
)
self.update_status.emit(self.repo)
if QtCore.QThread.currentThread().isInterruptionRequested():
return
self.readme_updated.emit(message)
@@ -971,9 +942,9 @@ class ShowWorker(QtCore.QThread):
storename = os.path.join(store, wbName + name[-remainChars:])
if not os.path.exists(storename):
try:
u = utils.urlopen(path)
imagedata = u.read()
u.close()
imagedata = AM_NETWORK_MANAGER.blocking_get(path)
if not imagedata:
raise Exception
except Exception:
FreeCAD.Console.PrintLog(
"AddonManager: Debug: Error retrieving image from "
@@ -987,7 +958,7 @@ class ShowWorker(QtCore.QThread):
# lower length limit for path
storename = storename[-140:]
f = open(storename, "wb")
f.write(imagedata)
f.write(imagedata.data())
f.close()
message = message.replace(
'src="' + origpath,
@@ -1095,7 +1066,15 @@ class InstallWorkbenchWorker(QtCore.QThread):
# Do the git process...
self.run_git(target_dir)
else:
self.run_zip(target_dir)
# The zip process uses an event loop, since the download can potentially be quite large
self.launch_zip(target_dir)
self.zip_complete = False
current_thread = QtCore.QThread.currentThread()
while not self.zip_complete:
if current_thread.isInterruptionRequested():
return
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
def update_status(self) -> None:
if hasattr(self, "git_progress") and self.isRunning():
@@ -1223,7 +1202,7 @@ class InstallWorkbenchWorker(QtCore.QThread):
self.update_metadata()
self.success.emit(self.repo, answer)
def run_zip(self, zipdir: str) -> None:
def launch_zip(self, zipdir: str) -> None:
"downloads and unzip a zip version from a git repo"
bakdir = None
@@ -1237,97 +1216,91 @@ class InstallWorkbenchWorker(QtCore.QThread):
if not zipurl:
self.failure.emit(
self.repo,
translate("AddonsInstaller", "Error: Unable to locate zip from")
translate("AddonsInstaller", "Error: Unable to locate ZIP from")
+ " "
+ self.repo.name,
)
return
try:
u = utils.urlopen(zipurl)
except Exception:
self.zipdir = zipdir
self.bakdir = bakdir
AM_NETWORK_MANAGER.progress_made.connect(self.update_zip_status)
AM_NETWORK_MANAGER.progress_complete.connect(self.finish_zip)
self.zip_download_index = AM_NETWORK_MANAGER.submit_monitored_get(zipurl)
def update_zip_status(self, index: int, bytes_read: int, data_size: int):
if index == self.zip_download_index:
locale = QtCore.QLocale()
if data_size > 10 * 1024 * 1024: # To avoid overflows, show MB instead
MB_read = bytes_read / 1024 / 1024
MB_total = data_size / 1024 / 1024
self.progress_made.emit(MB_read, MB_total)
mbytes_str = locale.toString(MB_read)
mbytes_total_str = locale.toString(MB_total)
percent = int(100 * float(MB_read / MB_total))
self.status_message.emit(
translate(
"AddonsInstaller",
f"Downloading: {mbytes_str}MB of {mbytes_total_str}MB ({percent}%)",
)
)
elif data_size > 0:
self.progress_made.emit(bytes_read, data_size)
bytes_str = locale.toString(bytes_read)
bytes_total_str = locale.toString(data_size)
percent = int(100 * float(bytes_read / data_size))
self.status_message.emit(
translate(
"AddonsInstaller",
f"Downloading: {bytes_str} of {bytes_total_str} bytes ({percent}%)",
)
)
else:
MB_read = bytes_read / 1024 / 1024
bytes_str = locale.toString(MB_read)
self.status_message.emit(
translate(
"AddonsInstaller",
f"Downloading: {bytes_str}MB of unknown total",
)
)
def finish_zip(self, index: int, response_code: int, filename: os.PathLike):
self.zip_complete = True
if response_code != 200:
self.failure.emit(
self.repo,
translate("AddonsInstaller", "Error: Unable to download")
+ " "
+ zipurl,
)
return
if not u:
self.failure.emit(
self.repo,
translate("AddonsInstaller", "Error: Unable to download")
+ " "
+ zipurl,
translate(
"AddonsInstaller",
f"Error: Error while downloading ZIP file for {self.repo.display_name}",
),
)
return
data_size = 0
if "content-length" in u.headers:
try:
data_size = int(u.headers["content-length"])
except Exception:
pass
current_thread = QtCore.QThread.currentThread()
# Use an on-disk file and track download progress
locale = QtCore.QLocale()
with tempfile.NamedTemporaryFile(delete=False) as temp:
bytes_to_read = 1024 * 1024
bytes_read = 0
while True:
if current_thread.isInterruptionRequested():
return
chunk = u.read(bytes_to_read)
if not chunk:
break
bytes_read += bytes_to_read
temp.write(chunk)
if data_size:
self.progress_made.emit(bytes_read, data_size)
percent = int(100 * float(bytes_read / data_size))
MB_read = bytes_read / 1024 / 1024
MB_total = data_size / 1024 / 1024
bytes_str = locale.toString(MB_read)
bytes_total_str = locale.toString(MB_total)
self.status_message.emit(
translate(
"AddonsInstaller",
f"Downloading: {bytes_str}MB of {bytes_total_str}MB ({percent}%)",
)
)
else:
MB_read = bytes_read / 1024 / 1024
bytes_str = locale.toString(MB_read)
self.status_message.emit(
translate(
"AddonsInstaller",
f"Downloading: {bytes_str}MB of unknown total",
)
)
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
zfile = zipfile.ZipFile(temp)
with zipfile.ZipFile(filename, "r") as zfile:
master = zfile.namelist()[0] # github will put everything in a subfolder
self.status_message.emit(
translate("AddonsInstaller", f"Download complete. Unzipping file...")
)
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents)
zfile.extractall(zipdir)
u.close()
zfile.close()
for filename in os.listdir(zipdir + os.sep + master):
shutil.move(
zipdir + os.sep + master + os.sep + filename,
zipdir + os.sep + filename,
)
os.rmdir(zipdir + os.sep + master)
if bakdir:
shutil.rmtree(bakdir)
self.update_metadata()
self.success.emit(
self.repo,
translate("AddonsInstaller", "Successfully installed") + " " + zipurl,
zfile.extractall(self.zipdir)
for filename in os.listdir(self.zipdir + os.sep + master):
shutil.move(
self.zipdir + os.sep + master + os.sep + filename,
self.zipdir + os.sep + filename,
)
os.rmdir(self.zipdir + os.sep + master)
if self.bakdir:
shutil.rmtree(self.bakdir)
self.update_metadata()
self.success.emit(
self.repo,
translate(
"AddonsInstaller",
f"Successfully installed {self.repo.display_name} from ZIP file",
),
)
def update_metadata(self):
basedir = FreeCAD.getUserAppDataDir()
@@ -1506,150 +1479,216 @@ class UpdateMetadataCacheWorker(QtCore.QThread):
progress_made = QtCore.Signal(int, int)
package_updated = QtCore.Signal(AddonManagerRepo)
class AtomicCounter(object):
def __init__(self, start=0):
self.lock = threading.Lock()
self.count = start
def set(self, new_value):
with self.lock:
self.count = new_value
def get(self):
with self.lock:
return self.count
def increment(self):
with self.lock:
self.count += 1
def decrement(self):
with self.lock:
self.count -= 1
class RequestType(Enum):
PACKAGE_XML = auto()
METADATA_TXT = auto()
REQUIREMENTS_TXT = auto()
ICON = auto()
def __init__(self, repos):
QtCore.QThread.__init__(self)
self.repos = repos
self.counter = UpdateMetadataCacheWorker.AtomicCounter()
self.requests: Dict[
int, (AddonManagerRepo, UpdateMetadataCacheWorker.RequestType)
] = {}
AM_NETWORK_MANAGER.completed.connect(self.download_completed)
self.requests_completed = 0
self.total_requests = 0
self.store = os.path.join(
FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata"
)
self.updated_repos = set()
def run(self):
current_thread = QtCore.QThread.currentThread()
self.num_downloads_required = len(self.repos)
self.progress_made.emit(0, self.num_downloads_required)
self.status_message.emit(
translate("AddonsInstaller", "Retrieving package metadata...")
)
store = os.path.join(
FreeCAD.getUserCachePath(), "AddonManager", "PackageMetadata"
)
index_file = os.path.join(store, "index.json")
self.index = {}
if os.path.isfile(index_file):
with open(index_file, "r") as f:
index_string = f.read()
self.index = json.loads(index_string)
download_queue = (
QtNetwork.QNetworkAccessManager()
) # Must be created on this thread
download_queue.finished.connect(self.on_finished)
# Prevent strange internal Qt errors about cache setup by pre-emptively setting
# up a cache. The error this fixes is:
# "caching was enabled after some bytes had been written"
qnam_cache = os.path.join(
FreeCAD.getUserCachePath(), "AddonManager", "QNAM_CACHE"
)
diskCache = QtNetwork.QNetworkDiskCache()
diskCache.setCacheDirectory(qnam_cache)
download_queue.setCache(diskCache)
self.downloaders = []
for repo in self.repos:
if repo.metadata_url:
if repo.url and utils.recognized_git_location(repo):
# package.xml
downloader = MetadataDownloadWorker(None, repo, self.index)
downloader.start_fetch(download_queue)
downloader.updated.connect(self.on_updated)
self.downloaders.append(downloader)
index = AM_NETWORK_MANAGER.submit_unmonitored_get(
utils.construct_git_url(repo, "package.xml")
)
self.requests[index] = (
repo,
UpdateMetadataCacheWorker.RequestType.PACKAGE_XML,
)
self.total_requests += 1
# metadata.txt
downloader = MetadataTxtDownloadWorker(None, repo)
downloader.start_fetch(download_queue)
downloader.updated.connect(self.on_updated)
self.downloaders.append(downloader)
index = AM_NETWORK_MANAGER.submit_unmonitored_get(
utils.construct_git_url(repo, "metadata.txt")
)
self.requests[index] = (
repo,
UpdateMetadataCacheWorker.RequestType.METADATA_TXT,
)
self.total_requests += 1
# requirements.txt
downloader = RequirementsTxtDownloadWorker(None, repo)
downloader.start_fetch(download_queue)
downloader.updated.connect(self.on_updated)
self.downloaders.append(downloader)
index = AM_NETWORK_MANAGER.submit_unmonitored_get(
utils.construct_git_url(repo, "requirements.txt")
)
self.requests[index] = (
repo,
UpdateMetadataCacheWorker.RequestType.REQUIREMENTS_TXT,
)
self.total_requests += 1
# Run a local event loop until we've processed all of the downloads:
# this is local to this thread, and does not affect the main event loop
ui_updater = QtCore.QTimer()
ui_updater.timeout.connect(self.send_ui_update)
ui_updater.start(100) # Send an update back to the main thread every 100ms
self.num_downloads_required = len(self.downloaders)
self.num_downloads_completed = UpdateMetadataCacheWorker.AtomicCounter()
aborted = False
while True:
while self.requests:
if current_thread.isInterruptionRequested():
download_queue.finished.disconnect(self.on_finished)
for downloader in self.downloaders:
downloader.updated.disconnect(self.on_updated)
downloader.abort()
aborted = True
if (
aborted
or self.num_downloads_completed.get() >= self.num_downloads_required
):
break
AM_NETWORK_MANAGER.completed.disconnect(self.download_completed)
for request in self.requests.keys():
AM_NETWORK_MANAGER.abort(request)
return
# 50 ms maximum between checks for interruption
QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 50)
if aborted:
FreeCAD.Console.PrintLog("Metadata update cancelled\n")
return
# This set contains one copy of each of the repos that got some kind of data in
# this process. For those repos, tell the main Addon Manager code that it needs
# to update its copy of the repo, and redraw its information.
for repo in self.updated_repos:
self.package_updated.emit(repo)
# Update and serialize the updated index, overwriting whatever was
# there before
for downloader in self.downloaders:
if hasattr(downloader, "last_sha1"):
self.index[downloader.repo.name] = downloader.last_sha1
if not os.path.exists(store):
os.makedirs(store)
with open(index_file, "w") as f:
json.dump(self.index, f, indent=" ")
def download_completed(
self, index: int, code: int, data: QtCore.QByteArray
) -> None:
if index in self.requests:
self.requests_completed += 1
self.progress_made.emit(self.requests_completed, self.total_requests)
request = self.requests.pop(index)
if code == 200: # HTTP success
self.updated_repos.add(request[0]) # mark this repo as updated
if request[1] == UpdateMetadataCacheWorker.RequestType.PACKAGE_XML:
self.process_package_xml(request[0], data)
elif request[1] == UpdateMetadataCacheWorker.RequestType.METADATA_TXT:
self.process_metadata_txt(request[0], data)
elif (
request[1] == UpdateMetadataCacheWorker.RequestType.REQUIREMENTS_TXT
):
self.process_requirements_txt(request[0], data)
elif request[1] == UpdateMetadataCacheWorker.RequestType.ICON:
self.process_icon(request[0], data)
def on_finished(self, reply):
# Called by the QNetworkAccessManager's sub-threads when a fetch
# process completed (in any state)
self.num_downloads_completed.increment()
reply.deleteLater()
def on_updated(self, repo):
# Called if this repo got new metadata and/or a new icon
self.package_updated.emit(repo)
def send_ui_update(self):
completed = self.num_downloads_completed.get()
required = self.num_downloads_required
percentage = int(100 * completed / required)
self.progress_made.emit(completed, required)
def process_package_xml(self, repo: AddonManagerRepo, data: QtCore.QByteArray):
repo.repo_type = AddonManagerRepo.RepoType.PACKAGE # By definition
package_cache_directory = os.path.join(self.store, repo.name)
if not os.path.exists(package_cache_directory):
os.makedirs(package_cache_directory)
new_xml_file = os.path.join(package_cache_directory, "package.xml")
with open(new_xml_file, "wb") as f:
f.write(data.data())
metadata = FreeCAD.Metadata(new_xml_file)
repo.metadata = metadata
self.status_message.emit(
translate("AddonsInstaller", "Retrieving package metadata...")
+ f" {completed} / {required} ({percentage}%)"
translate("AddonsInstaller", f"Downloaded package.xml for {repo.name}")
)
def terminate_all(self):
got = self.num_downloads_completed.get()
wanted = self.num_downloads_required
if wanted < got:
FreeCAD.Console.PrintWarning(
f"During cache interruption, wanted {wanted}, got {got}, forcibly terminating now...\n"
# Grab a new copy of the icon as well: we couldn't enqueue this earlier because
# we didn't know the path to it, which is stored in the package.xml file.
icon = metadata.Icon
if not icon:
# If there is no icon set for the entire package, see if there are
# any workbenches, which are required to have icons, and grab the first
# one we find:
content = repo.metadata.Content
if "workbench" in content:
wb = content["workbench"][0]
if wb.Icon:
if wb.Subdirectory:
subdir = wb.Subdirectory
else:
subdir = wb.Name
repo.Icon = subdir + wb.Icon
icon = repo.Icon
icon_url = utils.construct_git_url(repo, icon)
index = AM_NETWORK_MANAGER.submit_unmonitored_get(icon_url)
self.requests[index] = (repo, UpdateMetadataCacheWorker.RequestType.ICON)
self.total_requests += 1
def process_metadata_txt(self, repo: AddonManagerRepo, data: QtCore.QByteArray):
self.status_message.emit(
translate(
"AddonsInstaller", f"Downloaded metadata.txt for {repo.display_name}"
)
self.num_downloads_completed.set(wanted)
)
f = io.StringIO(data.data().decode("utf8"))
while True:
line = f.readline()
if not line:
break
if line.startswith("workbenches="):
depswb = line.split("=")[1].split(",")
for wb in depswb:
wb_name = wb.strip()
if wb_name:
repo.requires.add(wb_name)
FreeCAD.Console.PrintLog(
f"{repo.display_name} requires FreeCAD Addon '{wb_name}'\n"
)
elif line.startswith("pylibs="):
depspy = line.split("=")[1].split(",")
for pl in depspy:
dep = pl.strip()
if dep:
repo.python_requires.add(dep)
FreeCAD.Console.PrintLog(
f"{repo.display_name} requires python package '{dep}'\n"
)
elif line.startswith("optionalpylibs="):
opspy = line.split("=")[1].split(",")
for pl in opspy:
dep = pl.strip()
if dep:
repo.python_optional.add(dep)
FreeCAD.Console.PrintLog(
f"{repo.display_name} optionally imports python package '{pl.strip()}'\n"
)
# For review and debugging purposes, store the file locally
package_cache_directory = os.path.join(self.store, repo.name)
if not os.path.exists(package_cache_directory):
os.makedirs(package_cache_directory)
new_xml_file = os.path.join(package_cache_directory, "metadata.txt")
with open(new_xml_file, "wb") as f:
f.write(data.data())
def process_requirements_txt(self, repo: AddonManagerRepo, data: QtCore.QByteArray):
self.status_message.emit(
translate(
"AddonsInstaller",
f"Downloaded requirements.txt for {repo.display_name}",
)
)
f = io.StringIO(data.data().decode("utf8"))
lines = f.readlines()
for line in lines:
break_chars = " <>=~!+#"
package = line
for n, c in enumerate(line):
if c in break_chars:
package = line[:n].strip()
break
if package:
repo.python_requires.add(package)
# For review and debugging purposes, store the file locally
package_cache_directory = os.path.join(self.store, repo.name)
if not os.path.exists(package_cache_directory):
os.makedirs(package_cache_directory)
new_xml_file = os.path.join(package_cache_directory, "requirements.txt")
with open(new_xml_file, "wb") as f:
f.write(data.data())
def process_icon(self, repo: AddonManagerRepo, data: QtCore.QByteArray):
self.status_message.emit(
translate("AddonsInstaller", f"Downloaded icon for {repo.display_name}")
)
cache_file = repo.get_cached_icon_filename()
with open(cache_file, "wb") as icon_file:
icon_file.write(data.data())
repo.cached_icon_filename = cache_file
if have_git and not NOGIT: