Addon Manager: Add fetching of 'score' database

This commit is contained in:
Chris Hennes
2024-02-25 11:58:27 -06:00
parent 7e0cd689a5
commit 7d882ca37f
5 changed files with 116 additions and 30 deletions

View File

@@ -168,6 +168,7 @@ class Addon:
self.tags = set() # Just a cache, loaded from Metadata
self.last_updated = None
self.stats = AddonStats()
self.score = 0
# To prevent multiple threads from running git actions on this repo at the
# same time
@@ -260,7 +261,6 @@ class Addon:
def _process_date_string_to_python_datetime(self, date_string: str) -> datetime:
split_result = re.split(r"[ ./-]+", date_string.strip())
print(f"{self.display_name} - {split_result}")
if len(split_result) != 3:
raise SyntaxError(
f"In macro {self.name}, unrecognized date string '{date_string}' (expected YYYY-MM-DD)"

View File

@@ -43,6 +43,7 @@ from addonmanager_workers_startup import (
CheckWorkbenchesForUpdatesWorker,
CacheMacroCodeWorker,
GetBasicAddonStatsWorker,
GetAddonScoreWorker,
)
from addonmanager_workers_installation import (
UpdateMetadataCacheWorker,
@@ -119,6 +120,7 @@ class CommandAddonManager:
"update_all_worker",
"check_for_python_package_updates_worker",
"get_basic_addon_stats_worker",
"get_addon_score_worker",
]
lock = threading.Lock()
@@ -202,17 +204,17 @@ class CommandAddonManager:
self.button_bar.update_all_addons.hide()
# Set up the listing of packages using the model-view-controller architecture
self.packageList = PackageList(self.dialog)
self.package_list = PackageList(self.dialog)
self.item_model = PackageListItemModel()
self.packageList.setModel(self.item_model)
self.dialog.layout().addWidget(self.packageList)
self.package_list.setModel(self.item_model)
self.dialog.layout().addWidget(self.package_list)
self.dialog.layout().addWidget(self.button_bar)
# Package details start out hidden
self.packageDetails = PackageDetailsView(self.dialog)
self.package_details_controller = PackageDetailsController(self.packageDetails)
self.packageDetails.hide()
index = self.dialog.layout().indexOf(self.packageList)
index = self.dialog.layout().indexOf(self.package_list)
self.dialog.layout().insertWidget(index, self.packageDetails)
# set nice icons to everything, by theme with fallback to FreeCAD icons
@@ -241,9 +243,9 @@ class CommandAddonManager:
)
self.button_bar.python_dependencies.clicked.connect(self.show_python_updates_dialog)
self.button_bar.developer_tools.clicked.connect(self.show_developer_tools)
self.packageList.ui.progressBar.stop_clicked.connect(self.stop_update)
self.packageList.itemSelected.connect(self.table_row_activated)
self.packageList.setEnabled(False)
self.package_list.ui.progressBar.stop_clicked.connect(self.stop_update)
self.package_list.itemSelected.connect(self.table_row_activated)
self.package_list.setEnabled(False)
self.package_details_controller.execute.connect(self.executemacro)
self.package_details_controller.install.connect(self.launch_installer_gui)
self.package_details_controller.uninstall.connect(self.remove)
@@ -397,6 +399,7 @@ class CommandAddonManager:
self.check_updates,
self.check_python_updates,
self.fetch_addon_stats,
self.fetch_addon_score,
]
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
if pref.GetBool("DownloadMacros", False):
@@ -425,7 +428,7 @@ class CommandAddonManager:
)
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
pref.SetString("LastCacheUpdate", date.today().isoformat())
self.packageList.item_filter.invalidateFilter()
self.package_list.item_filter.invalidateFilter()
def populate_packages_table(self) -> None:
self.item_model.clear()
@@ -478,8 +481,8 @@ class CommandAddonManager:
f.write(json.dumps(self.package_cache, indent=" "))
def activate_table_widgets(self) -> None:
self.packageList.setEnabled(True)
self.packageList.ui.view_bar.search.setFocus()
self.package_list.setEnabled(True)
self.package_list.ui.view_bar.search.setFocus()
self.do_next_startup_phase()
def populate_macros(self) -> None:
@@ -682,6 +685,28 @@ class CommandAddonManager:
def update_addon_stats(self, addon: Addon):
self.item_model.reload_item(addon)
def fetch_addon_score(self) -> None:
"""Fetch the Addon score JSON data from a URL"""
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
url = pref.GetString("AddonsScoreURL", "NONE")
if url and url != "NONE":
self.get_addon_score_worker = GetAddonScoreWorker(
url, self.item_model.repos, self.dialog
)
self.get_addon_score_worker.finished.connect(self.score_fetched_successfully)
self.get_addon_score_worker.finished.connect(self.do_next_startup_phase)
self.get_addon_score_worker.update_addon_score.connect(self.update_addon_score)
self.get_addon_score_worker.start()
else:
self.package_list.ui.view_bar.set_rankings_available(False)
self.do_next_startup_phase()
def update_addon_score(self, addon: Addon):
self.item_model.reload_item(addon)
def score_fetched_successfully(self):
self.package_list.ui.view_bar.set_rankings_available(True)
def show_developer_tools(self) -> None:
"""Display the developer tools dialog"""
if not self.developer_mode:
@@ -758,24 +783,24 @@ class CommandAddonManager:
def table_row_activated(self, selected_repo: Addon) -> None:
"""a row was activated, show the relevant data"""
self.packageList.hide()
self.package_list.hide()
self.packageDetails.show()
self.package_details_controller.show_repo(selected_repo)
def show_information(self, message: str) -> None:
"""shows generic text in the information pane"""
self.packageList.ui.progressBar.set_status(message)
self.packageList.ui.progressBar.repaint()
self.package_list.ui.progressBar.set_status(message)
self.package_list.ui.progressBar.repaint()
def show_workbench(self, repo: Addon) -> None:
self.packageList.hide()
self.package_list.hide()
self.packageDetails.show()
self.package_details_controller.show_repo(repo)
def on_buttonBack_clicked(self) -> None:
self.packageDetails.hide()
self.packageList.show()
self.package_list.show()
def append_to_repos_list(self, repo: Addon) -> None:
"""this function allows threads to update the main list of workbenches"""
@@ -836,12 +861,12 @@ class CommandAddonManager:
def hide_progress_widgets(self) -> None:
"""hides the progress bar and related widgets"""
self.packageList.ui.progressBar.hide()
self.packageList.ui.view_bar.search.setFocus()
self.package_list.ui.progressBar.hide()
self.package_list.ui.view_bar.search.setFocus()
def show_progress_widgets(self) -> None:
if self.packageList.ui.progressBar.isHidden():
self.packageList.ui.progressBar.show()
if self.package_list.ui.progressBar.isHidden():
self.package_list.ui.progressBar.show()
def update_progress_bar(self, current_value: int, max_value: int) -> None:
"""Update the progress bar, showing it if it's hidden"""
@@ -858,10 +883,10 @@ class CommandAddonManager:
completed_region_portion = (self.current_progress_region - 1) * region_size
current_region_portion = (float(current_value) / float(max_value)) * region_size
value = completed_region_portion + current_region_portion
self.packageList.ui.progressBar.set_value(
self.package_list.ui.progressBar.set_value(
value * 10
) # Out of 1000 segments, so it moves sort of smoothly
self.packageList.ui.progressBar.repaint()
self.package_list.ui.progressBar.repaint()
def stop_update(self) -> None:
self.cleanup_workers()

View File

@@ -25,6 +25,11 @@
from enum import IntEnum, auto
try:
import FreeCAD
except ImportError:
FreeCAD = None
# Get whatever version of PySide we can
try:
import PySide # Use the FreeCAD wrapper
@@ -52,7 +57,7 @@ class SortOptions(IntEnum):
LastUpdated = QtCore.Qt.UserRole + _SortRoleOffset + 1
DateAdded = QtCore.Qt.UserRole + _SortRoleOffset + 2
Stars = QtCore.Qt.UserRole + _SortRoleOffset + 3
Rank = QtCore.Qt.UserRole + _SortRoleOffset + 4
Score = QtCore.Qt.UserRole + _SortRoleOffset + 4
default_sort_order = {
@@ -60,7 +65,7 @@ default_sort_order = {
SortOptions.LastUpdated: QtCore.Qt.DescendingOrder,
SortOptions.DateAdded: QtCore.Qt.DescendingOrder,
SortOptions.Stars: QtCore.Qt.DescendingOrder,
SortOptions.Rank: QtCore.Qt.DescendingOrder,
SortOptions.Score: QtCore.Qt.DescendingOrder,
}
@@ -75,6 +80,7 @@ class WidgetViewControlBar(QtWidgets.QWidget):
def __init__(self, parent: QtWidgets.QWidget = None):
super().__init__(parent)
self.has_rankings = False
self._setup_ui()
self._setup_connections()
self.retranslateUi(None)
@@ -124,6 +130,10 @@ class WidgetViewControlBar(QtWidgets.QWidget):
)
)
def set_rankings_available(self, rankings_available: bool) -> None:
self.has_rankings = rankings_available
self.retranslateUi(None)
def _setup_connections(self):
self.view_selector.view_changed.connect(self.view_changed.emit)
self.filter_selector.filter_changed.connect(self.filter_changed.emit)
@@ -133,6 +143,8 @@ class WidgetViewControlBar(QtWidgets.QWidget):
def _sort_changed(self, index: int):
sort_role = self.sort_selector.itemData(index)
if sort_role is None:
sort_role = SortOptions.Alphabetical
self.set_sort_order(default_sort_order[sort_role])
self.sort_changed.emit(sort_role)
self.sort_order_changed.emit(self.sort_order)
@@ -151,5 +163,7 @@ class WidgetViewControlBar(QtWidgets.QWidget):
self.sort_selector.addItem(
translate("AddonsInstaller", "GitHub Stars", "Sort order"), SortOptions.Stars
)
# self.sort_selector.addItem(translate("AddonsInstaller", "Rank", "Sort order"),
# SortOptions.Rank)
if self.has_rankings:
self.sort_selector.addItem(
translate("AddonsInstaller", "Score", "Sort order"), SortOptions.Score
)

View File

@@ -945,3 +945,50 @@ class GetBasicAddonStatsWorker(QtCore.QThread):
if addon.url in json_result:
addon.stats = AddonStats.from_json(json_result[addon.url])
self.update_addon_stats.emit(addon)
class GetAddonScoreWorker(QtCore.QThread):
"""Fetch data from an addon score file."""
update_addon_score = QtCore.Signal(Addon)
def __init__(self, url: str, addons: List[Addon], parent: QtCore.QObject = None):
super().__init__(parent)
self.url = url
self.addons = addons
def run(self):
"""Fetch the remote data and load it into the addons"""
if self.url != "TEST":
fetch_result = NetworkManager.AM_NETWORK_MANAGER.blocking_get(self.url, 5000)
if fetch_result is None:
FreeCAD.Console.PrintError(
translate(
"AddonsInstaller",
"Failed to get Addon score from {} -- sorting by score will fail\n",
).format(self.url)
)
return
text_result = fetch_result.data().decode("utf8")
json_result = json.loads(text_result)
else:
FreeCAD.Console.PrintWarning("Running score generation in TEST mode...\n")
json_result = {}
for addon in self.addons:
json_result[addon.url] = len(addon.display_name)
for addon in self.addons:
score = None
if addon.url in json_result:
score = json_result[addon.url]
elif addon.name in json_result:
score = json_result[addon.name]
if score is not None:
try:
addon.score = int(score)
self.update_addon_score.emit(addon)
except (ValueError, OverflowError):
FreeCAD.Console.PrintLog(
f"Failed to convert score value '{score}' to an integer for addon {addon.name}"
)

View File

@@ -196,8 +196,8 @@ class PackageListItemModel(QtCore.QAbstractListModel):
if self.repos[row].stats and self.repos[row].stats.stars:
return self.repos[row].stats.stars
return 0
if role == SortOptions.Rank:
return len(self.repos[row].display_name)
if role == SortOptions.Score:
return self.repos[row].score
def headerData(self, _unused1, _unused2, _role=QtCore.Qt.DisplayRole):
"""No header in this implementation: always returns None."""
@@ -386,8 +386,8 @@ class PackageListItemDelegate(QtWidgets.QStyledItemDelegate):
time_string = QtCore.QLocale().toString(qdt, QtCore.QLocale.ShortFormat)
return translate("AddonsInstaller", "Updated ") + time_string
return ""
elif self.sort_order == SortOptions.Rank:
return translate("AddonsInstaller", "Rank: ") + str(len(addon.display_name))
elif self.sort_order == SortOptions.Score:
return translate("AddonsInstaller", "Score: ") + str(len(addon.display_name))
return ""
def _set_sort_string_expanded(self, addon: Addon, label: QtWidgets.QLabel) -> None: