diff --git a/qt_ui/mainWindow.py b/qt_ui/mainWindow.py index 195fb2b..110198e 100644 --- a/qt_ui/mainWindow.py +++ b/qt_ui/mainWindow.py @@ -1,21 +1,31 @@ - +from copy import copy import os import sys from typing import List -#from speckle.converter.layers import getLayers -#import ui.speckle_qgis_dialog +# from speckle.speckle.converter.layers import getLayers -from specklepy.logging.exceptions import (SpeckleException, GraphQLException) +# import ui.speckle_qgis_dialog + +from specklepy.logging.exceptions import SpeckleException, GraphQLException from PyQt5 import QtWidgets, uic from PyQt5 import QtGui -from PyQt5.QtGui import QIcon, QPixmap -from PyQt5.QtWidgets import (QMainWindow, QApplication, QWidget, - QListWidgetItem, QAction, QDockWidget, QVBoxLayout, - QHBoxLayout, QWidget, QLabel) +from PyQt5.QtGui import QIcon, QPixmap, QCursor +from PyQt5.QtWidgets import ( + QMainWindow, + QApplication, + QWidget, + QListWidgetItem, + QAction, + QDockWidget, + QVBoxLayout, + QHBoxLayout, + QWidget, + QLabel, +) from PyQt5 import QtCore -from PyQt5.QtCore import pyqtSignal, Qt, QSize, QEvent -from PyQt5 import QtGui, uic +from PyQt5.QtCore import pyqtSignal, Qt, QSize, QEvent +from PyQt5 import QtGui, uic from specklepy.api.credentials import get_local_accounts @@ -25,54 +35,53 @@ from specklepy.api.wrapper import StreamWrapper from specklepy.api.client import SpeckleClient from specklepy.logging import metrics -import arcpy +import arcpy -import inspect - -try: - #from speckle.speckle_arcgis_new import Speckle - from speckle.converter.layers import getLayers, getAllProjLayers - from speckle.ui.logger import logToUser - from speckle.ui.LogWidget import LogWidget -except: - #from speckle_toolbox.esri.toolboxes.speckle.speckle_arcgis_new import Speckle - from speckle_toolbox.esri.toolboxes.speckle.converter.layers import getLayers, getAllProjLayers - from speckle_toolbox.esri.toolboxes.speckle.ui.logger import logToUser - from speckle_toolbox.esri.toolboxes.speckle.ui.LogWidget import LogWidget - -#from ui.validation import tryGetStream - -# Create module-like object -#pytPath = os.path.dirname(os.path.abspath(__file__)).replace("/speckle/ui","/Speckle.pyt") -#print(pytPath) -#pytModule = importlib.machinery.SourceFileLoader("specklePyt", pytPath ) -#specklePyt = pytModule.load_module("specklePyt") +import inspect +# from speckle.speckle_arcgis_new import Speckle +from speckle.speckle.converter.layers import getLayers, getAllProjLayers +from speckle.speckle.utils.panel_logging import logToUser +from speckle.specklepy_qt_ui.qt_ui.LogWidget import LogWidget +from speckle.specklepy_qt_ui.qt_ui.utils.utils import constructCommitURL # This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer -try: - from specklepy_qt_ui.qt_ui.global_resources import ( - COLOR_HIGHLIGHT, - SPECKLE_COLOR, SPECKLE_COLOR_LIGHT, - ICON_LOGO, ICON_SEARCH, ICON_DELETE, ICON_DELETE_BLUE, - ICON_SEND, ICON_RECEIVE, ICON_SEND_BLACK, ICON_RECEIVE_BLACK, - ICON_SEND_BLUE, ICON_RECEIVE_BLUE, - COLOR, BACKGR_COLOR, BACKGR_COLOR_LIGHT, - ) -except ModuleNotFoundError: - from speckle.specklepy_qt_ui.qt_ui.global_resources import ( - COLOR_HIGHLIGHT, - SPECKLE_COLOR, SPECKLE_COLOR_LIGHT, - ICON_LOGO, ICON_SEARCH, ICON_DELETE, ICON_DELETE_BLUE, - ICON_SEND, ICON_RECEIVE, ICON_SEND_BLACK, ICON_RECEIVE_BLACK, - ICON_SEND_BLUE, ICON_RECEIVE_BLUE, - COLOR, BACKGR_COLOR, BACKGR_COLOR_LIGHT, - ) -ui_class = uic.loadUiType( - os.path.join(os.path.dirname(__file__), os.path.join("ui", "mainWindow.ui") ) +from speckle.specklepy_qt_ui.qt_ui.utils.global_resources import ( + COLOR_HIGHLIGHT, + SPECKLE_COLOR, + SPECKLE_COLOR_LIGHT, + ICON_OPEN_WEB, + ICON_REPORT, + ICON_LOGO, + ICON_SEARCH, + ICON_DELETE, + ICON_DELETE_BLUE, + ICON_SEND, + ICON_RECEIVE, + ICON_SEND_BLACK, + ICON_RECEIVE_BLACK, + ICON_SEND_BLUE, + ICON_RECEIVE_BLUE, + COLOR, + BACKGR_COLOR, + BACKGR_COLOR_LIGHT, + ICON_XXL, + ICON_RASTER, + ICON_POLYGON, + ICON_LINE, + ICON_POINT, + ICON_GENERIC, ) +ui_file = uic.loadUiType( + os.path.join(os.path.dirname(__file__), os.path.join("ui", "mainWindow_main.ui")) +) +ui_file_path = os.path.join( + os.path.dirname(__file__), os.path.join("ui", "mainWindow_main.ui") +) + + class SpeckleGISDialog(QMainWindow): closingPlugin = pyqtSignal() @@ -88,161 +97,247 @@ class SpeckleGISDialog(QMainWindow): msgLog: LogWidget = None gridLayoutTitleBar = QtWidgets.QGridLayout - - def __init__(self): + + def __init__(self, parent=None): """Constructor.""" print("START MAIN WINDOW") - super(SpeckleGISDialog, self).__init__(None)#, QtCore.Qt.WindowStaysOnTopHint) - uic.loadUi(ui_class, self) # Load the .ui file - #self.installEventFilter(self) - self.show() - #self.instances.append(1) + super(SpeckleGISDialog, self).__init__( + parent + ) # , QtCore.Qt.WindowStaysOnTopHint) + uic.loadUi(ui_file_path, self) # Load the .ui file + # self.show() + self.runAllSetup() + + def runAllSetup(self): try: self.streamBranchDropdown.setMaxCount(100) self.commitDropdown.setMaxCount(100) self.streams_add_button.setFlat(True) self.streams_remove_button.setFlat(True) - self.saveSurveyPoint.setFlat(True) + self.commit_web_view.setFlat(True) + self.reportBtn.setFlat(True) + # self.saveSurveyPoint.setFlat(True) self.saveLayerSelection.setFlat(True) self.reloadButton.setFlat(True) self.closeButton.setFlat(True) + self.commit_web_view.setEnabled(False) - #backgr_color = f"background-color: rgb{str(SPECKLE_COLOR)};" - #backgr_color_light = f"background-color: rgb{str(SPECKLE_COLOR_LIGHT)};" + # https://stackoverflow.com/questions/67585501/pyqt-how-to-use-hover-in-button-stylesheet backgr_image_del = f"border-image: url({ICON_DELETE_BLUE});" self.streams_add_button.setIcon(QIcon(ICON_SEARCH)) self.streams_add_button.setMaximumWidth(25) - self.streams_add_button.setStyleSheet("QPushButton {padding:3px;padding-left:5px;border: none; text-align: left;} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + f"{COLOR}" + " }") + self.streams_add_button.setStyleSheet( + "QPushButton {padding:3px;padding-left:5px;border: none; text-align: left;} QPushButton:hover { " + + f"background-color: rgba{str(COLOR_HIGHLIGHT)};" + + f"{COLOR}" + + " }" + ) + + self.commit_web_view.setIcon(QIcon(ICON_OPEN_WEB)) + self.commit_web_view.setMaximumWidth(25) + self.commit_web_view.setStyleSheet( + "QPushButton {padding:3px;padding-left:5px;border: none; text-align: left;} QPushButton:hover { " + + f"background-color: rgba{str(COLOR_HIGHLIGHT)};" + + f"{COLOR}" + + " }" + ) + + self.reportBtn.setIcon(QIcon(ICON_REPORT)) + self.reportBtn.setMaximumWidth(25) + self.reportBtn.setStyleSheet( + "QPushButton {padding:3px;padding-left:5px;border: none; text-align: left;} QPushButton:hover { " + + f"background-color: rgba{str(COLOR_HIGHLIGHT)};" + + f"{COLOR}" + + " }" + ) + self.streams_remove_button.setIcon(QIcon(ICON_DELETE)) self.streams_remove_button.setMaximumWidth(25) - self.streams_remove_button.setStyleSheet("QPushButton {padding:3px;padding-left:5px;border: none; text-align: left; image-position:right} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + f"{COLOR}" + " }") #+ f"{backgr_image_del}" + self.streams_remove_button.setStyleSheet( + "QPushButton {padding:3px;padding-left:5px;border: none; text-align: left; image-position:right} QPushButton:hover { " + + f"background-color: rgba{str(COLOR_HIGHLIGHT)};" + + f"{COLOR}" + + " }" + ) # + f"{backgr_image_del}" - self.saveLayerSelection.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") - self.saveSurveyPoint.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") - self.reloadButton.setStyleSheet("QPushButton {text-align: left;} QPushButton:hover { " + f"{COLOR}" + " }") - self.closeButton.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + self.saveLayerSelection.setStyleSheet( + "QPushButton {text-align: right;} QPushButton:hover { " + + f"{COLOR}" + + " }" + ) + # self.saveSurveyPoint.setStyleSheet("QPushButton {text-align: right;} QPushButton:hover { " + f"{COLOR}" + " }") + self.reloadButton.setStyleSheet( + "QPushButton {text-align: left;} QPushButton:hover { " + + f"{COLOR}" + + " }" + ) + self.closeButton.setStyleSheet( + "QPushButton {text-align: right;} QPushButton:hover { " + + f"{COLOR}" + + " }" + ) - - self.sendModeButton.setStyleSheet("QPushButton {padding: 10px; border: 0px; " + f"color: rgb{str(SPECKLE_COLOR)};"+ "} QPushButton:hover { " + "}" ) + self.sendModeButton.setStyleSheet( + "QPushButton {padding: 10px; border: 0px; " + + f"color: rgba{str(SPECKLE_COLOR)};" + + "} QPushButton:hover { " + + "}" + ) self.sendModeButton.setIcon(QIcon(ICON_SEND_BLUE)) - + self.receiveModeButton.setFlat(True) - self.receiveModeButton.setStyleSheet("QPushButton {padding: 10px; border: 0px;}"+ "QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + "}" ) + self.receiveModeButton.setStyleSheet( + "QPushButton {padding: 10px; border: 0px;}" + + "QPushButton:hover { " + + f"background-color: rgba{str(COLOR_HIGHLIGHT)};" + + "}" + ) self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLACK)) - self.runButton.setStyleSheet("QPushButton {color: white;border: 0px;border-radius: 17px;padding: 10px;"+ f"{BACKGR_COLOR}" + "} QPushButton:hover { "+ f"{BACKGR_COLOR_LIGHT}" + " }") + self.runButton.setStyleSheet( + "QPushButton {color: white;border: 0px;border-radius: 17px;padding: 10px;" + + f"{BACKGR_COLOR}" + + "} QPushButton:hover { " + + f"{BACKGR_COLOR_LIGHT}" + + " }" + ) + # self.runButton.setGeometry(0, 0, 150, 30) self.runButton.setMaximumWidth(200) self.runButton.setIcon(QIcon(ICON_SEND)) - # add widgets that will only show on event trigger - logWidget = LogWidget(parent=self) - self.layout().addWidget(logWidget) - self.msgLog = logWidget + # insert checkbox + l = self.verticalLayout + + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) + + def runSetup(self, plugin): + # self.addDataStorage(plugin) + self.addLabel(plugin) + self.addProps(plugin) + # self.createMappingDialog() - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) - def addProps(self, plugin): - self.msgLog.active_account = plugin.active_account - self.msgLog.speckle_version = plugin.version + try: + # add widgets that will only show on event trigger + logWidget = LogWidget(parent=self) + logWidget.dataStorage = self.dataStorage - def addLabel(self, plugin): + self.layout().addWidget(logWidget) + self.msgLog = logWidget + self.msgLog.dockwidget = self + self.msgLog.active_account = plugin.dataStorage.active_account + self.msgLog.speckle_version = plugin.version + + self.crsSettings.setFlat(True) + self.crsSettings.setStyleSheet( + "QPushButton {text-align: right;} QPushButton:hover { " + + f"{COLOR}" + + " }" + ) + except Exception as e: + logToUser(e, level=2, plugin=self) + + def addLabel(self, plugin): try: exitIcon = QPixmap(ICON_LOGO) exitActIcon = QIcon(exitIcon) - # create a label + # create a label text_label = QtWidgets.QPushButton(" for ArcGIS") - text_label.setStyleSheet("border: 0px;" - "color: white;" - f"{BACKGR_COLOR}" - "top-margin: 40 px;" - "padding: 10px;" - "padding-left: 20px;" - "font-size: 15px;" - "height: 30px;" - "text-align: left;" - ) + text_label.setStyleSheet( + "border: 0px;" + "color: white;" + f"{BACKGR_COLOR}" + "top-margin: 40 px;" + "padding: 10px;" + "padding-left: 20px;" + "font-size: 15px;" + "height: 30px;" + "text-align: left;" + ) text_label.setIcon(exitActIcon) - text_label.setIconSize(QSize(300, 93)) - text_label.setMinimumSize(QSize(100, 40)) - text_label.setMaximumWidth(220) + text_label.setIconSize(QtCore.QSize(300, 93)) + text_label.setMinimumSize(QtCore.QSize(100, 40)) + text_label.setMaximumWidth(250) version = "" - try: - if isinstance(plugin.version, str): version = str(plugin.version) - except: pass + try: + if isinstance(plugin.version, str): + version = str(plugin.version) + except: + pass - version_label = QtWidgets.QPushButton(f"{version}") - version_label.setStyleSheet("border: 0px;" - "color: white;" - f"{BACKGR_COLOR}" - "padding-top: 15px;" - "padding-left: 0px;" - "margin-left: 0px;" - "font-size: 10px;" - "height: 30px;" - "text-align: left;" - ) + version_label = QtWidgets.QPushButton(version) + version_label.setStyleSheet( + "border: 0px;" + "color: white;" + f"{BACKGR_COLOR}" + "padding-top: 15px;" + "padding-left: 0px;" + "margin-left: 0px;" + "font-size: 10px;" + "height: 30px;" + "text-align: left;" + ) widget = QWidget() widget.setStyleSheet(f"{BACKGR_COLOR}") - connect_box = QHBoxLayout(widget) - connect_box.addWidget(text_label) #, alignment=Qt.AlignCenter) - connect_box.addWidget(version_label) - connect_box.setContentsMargins(0, 0, 0, 0) - self.gridLayoutTitleBar.addWidget(widget) # fro QMainWindow - #self.setTitleBarWidget(widget) # for QDockWidget + boxLayout = QHBoxLayout(widget) + boxLayout.addWidget(text_label) # , alignment=Qt.AlignCenter) + boxLayout.addWidget(version_label) + boxLayout.setContentsMargins(0, 0, 0, 0) + self.setWindowTitle("SpeckleArcGIS") + self.gridLayoutTitleBar.addWidget(widget) # fro QMainWindow + # self.setTitleBarWidget(widget) # for dockwidget + self.labelWidget = text_label + self.labelWidget.setCursor(QCursor(QtCore.Qt.PointingHandCursor)) + self.labelWidget.clicked.connect(self.onClickLogo) except Exception as e: logToUser(e) + def addDataStorage(self, plugin): + self.dataStorage = plugin.dataStorage + try: + self.dataStorage.project = plugin.project + except: + self.dataStorage.project = plugin.qproject + def resizeEvent(self, event): try: - #print("resize") + # print("resize") QtWidgets.QMainWindow.resizeEvent(self, event) - if self.msgLog.size().height() != 0: # visible - self.msgLog.setGeometry(0, 0, self.msgLog.parentWidget.frameSize().width(), self.msgLog.parentWidget.frameSize().height()) #.resize(self.frameSize().width(), self.frameSize().height()) + if self.msgLog.size().height() != 0: # visible + self.msgLog.setGeometry( + 0, + 0, + self.msgLog.parentWidget.frameSize().width(), + self.msgLog.parentWidget.frameSize().height(), + ) # .resize(self.frameSize().width(), self.frameSize().height()) except Exception as e: - #logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + # logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) return - - def closeEvent(self, event): - try: - #import threading - print("Close event") - #threads = threading.enumerate() - #print(f"Threads total: {str(len(threads))}: {str(threads)}") - #print(self.instances) - - self.closingPlugin.emit() - event.accept() - - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) - def clearDropdown(self): try: - #self.streamIdField.clear() self.streamBranchDropdown.clear() self.commitDropdown.clear() - #self.layerSendModeDropdown.clear() - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + except Exception as e: + logToUser(e, level=2, func=inspect.stack()[0][3], plugin=self) + return def reloadDialogUI(self, plugin): try: self.clearDropdown() - self.populateUI(plugin) + self.populateUI(plugin) self.enableElements(plugin) - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + except Exception as e: + logToUser(e, level=2, func=inspect.stack()[0][3], plugin=self) + return - - - def run(self, plugin): + def run(self, plugin): try: print("dockwidget run") # Setup events on first load only! @@ -250,209 +345,336 @@ class SpeckleGISDialog(QMainWindow): # Connect streams section events self.completeStreamSection(plugin) # Populate the UI dropdowns - self.populateUI(plugin) + self.populateUI(plugin) print("dockwidget run end") - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) + def closeEvent(self, event): + try: + # import threading + print("Close event") + # threads = threading.enumerate() + # print(f"Threads total: {str(len(threads))}: {str(threads)}") + + # print(self.instances) + + self.closingPlugin.emit() + event.accept() + + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) + + def addMsg(self, obj: dict): + try: + self.msgLog.addButton(obj) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) def setupOnFirstLoad(self, plugin): try: - self.runButton.clicked.connect(plugin.onRunButtonClicked) - - self.streams_add_button.clicked.connect( plugin.onStreamAddButtonClicked ) + self.msgLog.sendMessage.connect(self.addMsg) + self.reportBtn.clicked.connect(self.msgLog.showReport) + + self.streams_add_button.clicked.connect(plugin.onStreamAddButtonClicked) + self.commit_web_view.clicked.connect( + lambda: plugin.openUrl( + constructCommitURL( + plugin.active_stream, + plugin.active_branch.id, + plugin.active_commit.id, + ) + ) + ) self.reloadButton.clicked.connect(lambda: self.refreshClicked(plugin)) self.closeButton.clicked.connect(lambda: self.closeClicked(plugin)) - self.saveSurveyPoint.clicked.connect(plugin.set_survey_point) - self.saveLayerSelection.clicked.connect(lambda: self.populateLayerDropdown(plugin)) + self.sendModeButton.clicked.connect(lambda: self.setSendMode(plugin)) - self.layerSendModeDropdown.currentIndexChanged.connect( lambda: self.layerSendModeChange(plugin) ) + self.layerSendModeDropdown.currentIndexChanged.connect( + lambda: self.layerSendModeChange(plugin) + ) self.receiveModeButton.clicked.connect(lambda: self.setReceiveMode(plugin)) - self.streamBranchDropdown.currentIndexChanged.connect( lambda: self.runBtnStatusChanged(plugin) ) - self.commitDropdown.currentIndexChanged.connect( lambda: self.runBtnStatusChanged(plugin) ) + # self.streamBranchDropdown.currentIndexChanged.connect( lambda: self.runBtnStatusChanged(plugin) ) + self.commitDropdown.currentIndexChanged.connect( + lambda: self.setActiveCommit(plugin) + ) + self.commitDropdown.currentIndexChanged.connect( + lambda: self.runBtnStatusChanged(plugin) + ) self.closingPlugin.connect(plugin.onClosePlugin) - return - - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + return + + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) + + def onClickLogo(self): + import webbrowser + + url = "https://speckle.systems/" + webbrowser.open(url, new=0, autoraise=True) + + try: + metrics.track( + "Connector Action", + self.dataStorage.active_account, + { + "name": "Logo Click", + "connector_version": str(self.dataStorage.plugin_version), + }, + ) + except Exception as e: + print(e) def refreshClicked(self, plugin): try: try: - metrics.track("Connector Action", plugin.active_account, {"name": "Refresh", "connector_version": str(plugin.version)}) + metrics.track( + "Connector Action", + plugin.dataStorage.active_account, + {"name": "Refresh", "connector_version": str(plugin.version)}, + ) except Exception as e: - logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=plugin.dockwidget ) - + logToUser( + e, level=2, func=inspect.stack()[0][3], plugin=plugin.dockwidget + ) + plugin.reloadUI() except Exception as e: - logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + logToUser(e, level=2, func=inspect.stack()[0][3], plugin=self) return def closeClicked(self, plugin): try: try: - metrics.track("Connector Action", plugin.active_account, {"name": "Close", "connector_version": str(plugin.version)}) + metrics.track( + "Connector Action", + plugin.dataStorage.active_account, + {"name": "Close", "connector_version": str(plugin.version)}, + ) except Exception as e: - logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=plugin.dockwidget ) - + logToUser( + e, level=2, func=inspect.stack()[0][3], plugin=plugin.dockwidget + ) + plugin.onClosePlugin() except Exception as e: - logToUser(e, level = 2, func = inspect.stack()[0][3], plugin=self) + logToUser(e, level=2, func=inspect.stack()[0][3], plugin=self) return def setSendMode(self, plugin): try: - plugin.btnAction = 0 # send - color = f"color: rgb{str(SPECKLE_COLOR)};" - self.sendModeButton.setStyleSheet("border: 0px;" - f"color: rgb{str(SPECKLE_COLOR)};" - "padding: 10px;") + plugin.btnAction = 0 # send + color = f"color: rgba{str(SPECKLE_COLOR)};" + self.sendModeButton.setStyleSheet( + "border: 0px;" f"color: rgba{str(SPECKLE_COLOR)};" "padding: 10px;" + ) self.sendModeButton.setIcon(QIcon(ICON_SEND_BLUE)) self.sendModeButton.setFlat(False) self.receiveModeButton.setFlat(True) - self.receiveModeButton.setStyleSheet("QPushButton {border: 0px; color: black; padding: 10px; } QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + " };") + self.receiveModeButton.setStyleSheet( + "QPushButton {border: 0px; color: black; padding: 10px; } QPushButton:hover { " + + f"background-color: rgba{str(COLOR_HIGHLIGHT)};" + + " };" + ) self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLACK)) - #self.receiveModeButton.setFlat(True) self.runButton.setProperty("text", " SEND") self.runButton.setIcon(QIcon(ICON_SEND)) - # enable sections only if in "saved streams" mode - if self.layerSendModeDropdown.currentIndex() == 1: self.layersWidget.setEnabled(True) - if self.layerSendModeDropdown.currentIndex() == 1: self.saveLayerSelection.setEnabled(True) + # enable sections only if in "saved streams" mode + if self.layerSendModeDropdown.currentIndex() == 1: + self.layersWidget.setEnabled(True) + self.saveLayerSelection.setEnabled(True) + self.commitLabel.setEnabled(False) self.commitDropdown.setEnabled(False) + self.messageLabel.setEnabled(True) self.messageInput.setEnabled(True) self.layerSendModeDropdown.setEnabled(True) + self.commit_web_view.setEnabled(False) self.runBtnStatusChanged(plugin) return - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) - + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) + def setReceiveMode(self, plugin): try: - plugin.btnAction = 1 # receive - color = f"color: rgb{str(SPECKLE_COLOR)};" - self.receiveModeButton.setStyleSheet("border: 0px;" - f"color: rgb{str(SPECKLE_COLOR)};" - "padding: 10px;") + plugin.btnAction = 1 # receive + color = f"color: rgba{str(SPECKLE_COLOR)};" + self.receiveModeButton.setStyleSheet( + "border: 0px;" f"color: rgba{str(SPECKLE_COLOR)};" "padding: 10px;" + ) self.sendModeButton.setIcon(QIcon(ICON_SEND_BLACK)) - self.sendModeButton.setStyleSheet("QPushButton {border: 0px; color: black; padding: 10px;} QPushButton:hover { " + f"background-color: rgb{str(COLOR_HIGHLIGHT)};" + " };") + self.sendModeButton.setStyleSheet( + "QPushButton {border: 0px; color: black; padding: 10px;} QPushButton:hover { " + + f"background-color: rgba{str(COLOR_HIGHLIGHT)};" + + " };" + ) self.receiveModeButton.setIcon(QIcon(ICON_RECEIVE_BLUE)) self.sendModeButton.setFlat(True) self.receiveModeButton.setFlat(False) - #self.sendModeButton.setFlat(True) self.runButton.setProperty("text", " RECEIVE") self.runButton.setIcon(QIcon(ICON_RECEIVE)) - #self.layerSendModeChange(plugin, 1) + self.commitLabel.setEnabled(True) self.commitDropdown.setEnabled(True) + self.layersWidget.setEnabled(False) + self.messageLabel.setEnabled(False) self.messageInput.setEnabled(False) self.saveLayerSelection.setEnabled(False) self.layerSendModeDropdown.setEnabled(False) + self.commit_web_view.setEnabled(True) self.runBtnStatusChanged(plugin) return - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) def completeStreamSection(self, plugin): - self.streams_remove_button.clicked.connect( lambda: self.onStreamRemoveButtonClicked(plugin) ) - self.streamList.currentIndexChanged.connect( lambda: self.onActiveStreamChanged(plugin) ) - self.streamBranchDropdown.currentIndexChanged.connect( lambda: self.populateActiveCommitDropdown(plugin) ) return def populateUI(self, plugin): try: self.populateLayerSendModeDropdown() - self.populateLayerDropdown(plugin, False) - #items = [self.layersWidget.item(x).text() for x in range(self.layersWidget.count())] self.populateProjectStreams(plugin) - self.populateSurveyPoint(plugin) - self.runBtnStatusChanged(plugin) - self.runButton.setEnabled(False) - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) - + # self.runBtnStatusChanged(plugin) + # self.runButton.setEnabled(False) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) + + def setActiveCommit(self, plugin): + try: + # print("__setActiveCommit") + # print(plugin.active_commit) + if plugin.active_branch is None: + if ( + plugin.active_stream is not None + and plugin.active_stream[1] is not None + ): + branchName = self.streamBranchDropdown.currentText() + for b in plugin.active_stream[1].branches.items: + if b.name == branchName: + branch = b + plugin.active_branch = b + break + + if plugin.active_branch is None: + return + # print(plugin.active_branch.name) + + current_id_text = str(self.commitDropdown.currentText()).split(" ")[0] + # print(current_id_text) + if current_id_text == "": # populate commits still in progress + return + + if len(plugin.active_branch.commits.items) > 0: + if "Latest" in current_id_text: + plugin.active_commit = plugin.active_branch.commits.items[0] + return + for c in plugin.active_branch.commits.items: + if c.id == current_id_text: + plugin.active_commit = c + return + # only if not found: + plugin.active_commit = plugin.active_branch.commits.items[0] + else: + plugin.active_commit = None + except Exception as e: + plugin.active_commit = None + print(e) + def runBtnStatusChanged(self, plugin): try: commitStr = str(self.commitDropdown.currentText()) branchStr = str(self.streamBranchDropdown.currentText()) - if plugin.btnAction == 1: # on receive - if commitStr == "": - self.runButton.setEnabled(False) - else: - self.runButton.setEnabled(True) - - if plugin.btnAction == 0: # on send - if branchStr == "": - self.runButton.setEnabled(False) - elif branchStr != "" and self.layerSendModeDropdown.currentIndex() == 1 and len(plugin.current_layers) == 0: # saved layers; but the list is empty + if commitStr == "": # populate commits still in progress + return + + if plugin.btnAction == 1: # on receive + if commitStr == "": self.runButton.setEnabled(False) else: self.runButton.setEnabled(True) - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) - - def layerSendModeChange(self, plugin, runMode = None): + if plugin.btnAction == 0: # on send + if branchStr == "": + self.runButton.setEnabled(False) + elif ( + self.layerSendModeDropdown.currentIndex() == 1 + and len(plugin.dataStorage.current_layers) == 0 + ): # saved layers; but the list is empty + self.runButton.setEnabled(False) + else: + self.runButton.setEnabled(True) + except Exception as e: + logToUser(e, level=2, func=inspect.stack()[0][3], plugin=self) + return + + def layerSendModeChange(self, plugin, runMode=None): try: print("Send mode changed") - if self.layerSendModeDropdown.currentIndex() == 0 or runMode == 1: # by manual selection OR receive mode - self.current_layers = [] + if ( + self.layerSendModeDropdown.currentIndex() == 0 or runMode == 1 + ): # by manual selection OR receive mode self.layersWidget.setEnabled(False) - self.saveLayerSelection.setEnabled(False) - - elif self.layerSendModeDropdown.currentIndex() == 1 and (runMode == 0 or runMode is None): # by saved AND when Send mode + + elif self.layerSendModeDropdown.currentIndex() == 1 and ( + runMode == 0 or runMode is None + ): # by saved AND when Send mode self.layersWidget.setEnabled(True) - self.saveLayerSelection.setEnabled(True) - + branchStr = str(self.streamBranchDropdown.currentText()) if self.layerSendModeDropdown.currentIndex() == 0: - if branchStr == "": self.runButton.setEnabled(False) # by manual selection - else: self.runButton.setEnabled(True) # by manual selection - elif self.layerSendModeDropdown.currentIndex() == 1: self.runBtnStatusChanged(plugin) # by saved - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + if branchStr == "": + self.runButton.setEnabled(False) # by manual selection + else: + self.runButton.setEnabled(True) # by manual selection + elif self.layerSendModeDropdown.currentIndex() == 1: + self.runBtnStatusChanged(plugin) # by saved + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) - def populateLayerDropdown(self, plugin, bySelection: bool = True): + def populateSavedLayerDropdown(self, plugin, bySelection: bool = True): print("populate layer dropdown / clicked save selection") - if not self: return - try: - from speckle.ui.project_vars import set_project_layer_selection - except: - from speckle_toolbox.esri.toolboxes.speckle.speckle.ui.project_vars import set_project_layer_selection - + if not self: + return + try: + from speckle.speckle.utils.project_vars import set_project_layer_selection + except: + from speckle_toolbox.esri.toolboxes.speckle.speckle.utils.project_vars import ( + set_project_layer_selection, + ) + try: self.layersWidget.clear() - nameDisplay = [] - project = plugin.gis_project + nameDisplay = [] + project = plugin.project - if bySelection is False: # read from project data + if bySelection is False: # read from project data print("populate layers from saved data") - #print(project) - #print(project.activeMap) + # print(project) + # print(project.activeMap) all_layers_ids = [l.dataSource for l in getAllProjLayers(project)] for layer_tuple in plugin.current_layers: - if layer_tuple[1].dataSource in all_layers_ids: - listItem = self.fillLayerList(layer_tuple[1]) + if layer_tuple[1].dataSource in all_layers_ids: + listItem = self.fillLayerList(layer_tuple[1]) self.layersWidget.addItem(listItem) - else: # read selected layers + else: # read selected layers # Fetch selected layers print("populate layers from selection") - + plugin.current_layers = [] - layers = getLayers(plugin, bySelection) # List[QgsLayerTreeNode] + layers = getLayers(plugin, bySelection) # List[QgsLayerTreeNode] print(layers) for i, layer in enumerate(layers): - plugin.current_layers.append((layer.name, layer)) + plugin.current_layers.append((layer.name, layer)) listItem = self.fillLayerList(layer) self.layersWidget.addItem(listItem) print("populate layers from selection 2") @@ -461,53 +683,50 @@ class SpeckleGISDialog(QMainWindow): self.layersWidget.setIconSize(QSize(20, 20)) self.runBtnStatusChanged(plugin) - + return - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) def fillLayerList(self, layer): print("Fill layer list") - + try: ICON_XXL = os.path.dirname(os.path.abspath(__file__)) + "/size-xxl.png" - ICON_RASTER = os.path.dirname(os.path.abspath(__file__)) + "/legend_raster.png" - ICON_POLYGON = os.path.dirname(os.path.abspath(__file__)) + "/legend_polygon.png" + ICON_RASTER = ( + os.path.dirname(os.path.abspath(__file__)) + "/legend_raster.png" + ) + ICON_POLYGON = ( + os.path.dirname(os.path.abspath(__file__)) + "/legend_polygon.png" + ) ICON_LINE = os.path.dirname(os.path.abspath(__file__)) + "/legend_line.png" - ICON_POINT = os.path.dirname(os.path.abspath(__file__)) + "/legend_point.png" + ICON_POINT = ( + os.path.dirname(os.path.abspath(__file__)) + "/legend_point.png" + ) - listItem = QListWidgetItem(layer.name) - #print(listItem) + listItem = QListWidgetItem(layer.name) + # print(listItem) - if layer.isRasterLayer: # and layer.width()*layer.height() > 1000000: + if layer.isRasterLayer: # and layer.width()*layer.height() > 1000000: listItem.setIcon(QIcon(ICON_RASTER)) - - elif layer.isFeatureLayer: # and layer.featureCount() > 20000: + + elif layer.isFeatureLayer: # and layer.featureCount() > 20000: geomType = arcpy.Describe(layer.dataSource).shapeType - if geomType == "Polygon": listItem.setIcon(QIcon(ICON_POLYGON)) - elif geomType == "Polyline": listItem.setIcon(QIcon(ICON_LINE)) - elif geomType == "Point" or geomType == "Multipoint": listItem.setIcon(QIcon(ICON_POINT)) - else: + if geomType == "Polygon": + listItem.setIcon(QIcon(ICON_POLYGON)) + elif geomType == "Polyline": + listItem.setIcon(QIcon(ICON_LINE)) + elif geomType == "Point" or geomType == "Multipoint": + listItem.setIcon(QIcon(ICON_POINT)) + else: listItem.setIcon(QIcon(ICON_XXL)) - #else: + # else: # icon = QgsIconUtils().iconForLayer(layer) # listItem.setIcon(icon) - + return listItem - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) - - def populateSurveyPoint(self, plugin): - if not self: - return - try: - self.surveyPointLat.clear() - self.surveyPointLat.setText(str(plugin.lat)) - self.surveyPointLon.clear() - self.surveyPointLon.setText(str(plugin.lon)) - - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) def enableElements(self, plugin): try: @@ -515,128 +734,212 @@ class SpeckleGISDialog(QMainWindow): self.receiveModeButton.setEnabled(plugin.is_setup) self.runButton.setEnabled(plugin.is_setup) self.streams_add_button.setEnabled(plugin.is_setup) - if plugin.is_setup is False: self.streams_remove_button.setEnabled(plugin.is_setup) + self.commit_web_view.setEnabled(plugin.active_commit is not None) + self.reportBtn.setEnabled(False) + if plugin.is_setup is False: + self.streams_remove_button.setEnabled(plugin.is_setup) self.streamBranchDropdown.setEnabled(plugin.is_setup) self.layerSendModeDropdown.setEnabled(plugin.is_setup) + self.commitLabel.setEnabled(False) self.commitDropdown.setEnabled(False) self.show() - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + self.setSendMode(plugin) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) def populateProjectStreams(self, plugin): - - try: - from speckle.ui.project_vars import set_project_streams - except: - from speckle_toolbox.esri.toolboxes.speckle.speckle.ui.project_vars import set_project_streams - - try: - if not self: return - self.streamList.clear() - for stream in plugin.current_streams: - self.streamList.addItems( - [f"Stream not accessible - {stream[0].stream_id}" if stream[1] is None or isinstance(stream[1], SpeckleException) else f"{stream[1].name}, {stream[1].id} | {stream[0].stream_url.split('/streams')[0]}"] - ) - if len(plugin.current_streams)==0: self.streamList.addItems([""]) - self.streamList.addItems(["Create New Stream"]) - set_project_streams(plugin) - index = self.streamList.currentIndex() - if index == -1: self.streams_remove_button.setEnabled(False) - else: self.streams_remove_button.setEnabled(True) - - if len(plugin.current_streams)>0: plugin.active_stream = plugin.current_streams[0] - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) - + return def onActiveStreamChanged(self, plugin): - if not self: return + if not self: + return try: + if not self: + return index = self.streamList.currentIndex() - if (len(plugin.current_streams) == 0 and index ==1) or (len(plugin.current_streams)>0 and index == len(plugin.current_streams)): + if (len(plugin.current_streams) == 0 and index == 1) or ( + len(plugin.current_streams) > 0 and index == len(plugin.current_streams) + ): self.populateProjectStreams(plugin) plugin.onStreamCreateClicked() return - if len(plugin.current_streams) == 0: return - if index == -1: return + if len(plugin.current_streams) == 0: + return + if index == -1: + return - try: plugin.active_stream = plugin.current_streams[index] - except: plugin.active_stream = None + try: + plugin.active_stream = plugin.current_streams[index] + except: + plugin.active_stream = None self.populateActiveStreamBranchDropdown(plugin) self.populateActiveCommitDropdown(plugin) - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) - + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) + def populateLayerSendModeDropdown(self): - if not self: return + if not self: + return try: self.layerSendModeDropdown.clear() self.layerSendModeDropdown.addItems( ["Send visible layers", "Send saved layers"] ) - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) def populateActiveStreamBranchDropdown(self, plugin): - if not self: return - if plugin.active_stream is None: return + if not self: + return + if plugin.active_stream is None: + return try: - self.streamBranchDropdown.clear() - if isinstance(plugin.active_stream[1], SpeckleException): - #logger.logToUser("Some streams cannot be accessed", Qgis.Warning) + active_branch = copy(plugin.active_branch) + active_commit = copy(plugin.active_commit) + keep_branch = True # case of search by URL + if active_branch is None: # case of populating from Saved Streams + keep_branch = False + # print(active_branch) + + # print(1) + self.streamBranchDropdown.clear() # activates "populate commit" + # print(2) + if isinstance(plugin.active_stream[1], SpeckleException): + logToUser("Some streams cannot be accessed", level=1, plugin=self) return - elif plugin.active_stream is None or plugin.active_stream[1] is None or plugin.active_stream[1].branches is None: + elif ( + plugin.active_stream is None + or plugin.active_stream[1] is None + or plugin.active_stream[1].branches is None + ): return + # print(3) + # print(plugin.active_branch) + + # here the commit dropdown is triggered self.streamBranchDropdown.addItems( [f"{branch.name}" for branch in plugin.active_stream[1].branches.items] ) + # print(4) self.streamBranchDropdown.addItems(["Create New Branch"]) - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + # print(5) + if keep_branch is True: + plugin.active_branch = active_branch + if active_commit is not None: + plugin.active_commit = active_commit + elif len(plugin.active_branch.commits.items) > 0: + plugin.active_commit = plugin.active_branch.commits.items[0] + # else: + # plugin.active_commit = plugin.active_branch.commits.items[0] + # print(plugin.active_branch) + + # set index to current (if added from URL) + if ( + plugin.active_branch is not None + and plugin.active_branch in plugin.active_stream[1].branches.items + ): + # print("__________SET BRANCH TEXT") + # print(plugin.active_branch.name) + if keep_branch is True: + plugin.active_branch = active_branch + plugin.active_commit = active_commit + # print(plugin.active_branch.name) + self.streamBranchDropdown.setCurrentText( + plugin.active_branch.name + ) # activates "populate commit" + # print(6) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) def populateActiveCommitDropdown(self, plugin): - if not self: return + if not self: + return try: - self.commitDropdown.clear() - if plugin.active_stream is None: return + if plugin.active_stream is None: + print("Active stream is None") + return branchName = self.streamBranchDropdown.currentText() - if branchName == "": return - if branchName == "Create New Branch": + # print(f"CURRENT BRANCH TEXT: {branchName}") + if branchName == "": + return + if branchName == "Create New Branch": self.streamBranchDropdown.setCurrentText("main") plugin.onBranchCreateClicked() return branch = None - if isinstance(plugin.active_stream[1], SpeckleException): - #logger.logToUser("Some streams cannot be accessed", Qgis.Warning) + + # print("__clear commit dropdwn") + # print(plugin.active_commit) + self.commitDropdown.clear() + if isinstance(plugin.active_stream[1], SpeckleException): + logToUser("Some streams cannot be accessed", level=1, plugin=self) return elif plugin.active_stream[1]: for b in plugin.active_stream[1].branches.items: if b.name == branchName: branch = b + plugin.active_branch = b break - try: - self.commitDropdown.addItems( - [f"{commit.id}"+ " | " + f"{commit.message}" for commit in branch.commits.items] - ) - except: pass - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + + if len(branch.commits.items) > 0: + commits = [] + commits.append("") + # commits.append("Latest commit from this branch") + # self.commitDropdown.addItem("Latest commit from this branch") + + # commits = [] + for commit in branch.commits.items: + sourceApp = ( + str(commit.sourceApplication) + .replace(" ", "") + .split(".")[0] + .split("-")[0] + ) + commits.append( + f"{commit.id}" + + " | " + + f"{sourceApp}" + + " | " + + f"{commit.message}" + ) + self.commitDropdown.addItems(commits) + + # set index to current (if added from URL) + if ( + plugin.active_commit is not None + and plugin.active_commit in branch.commits.items + ): + # print("set index to current (if added from URL) ") + # print(plugin.active_commit) + self.commitDropdown.setCurrentText( + f"{plugin.active_commit.id}" + + " | " + + f"{plugin.active_commit.sourceApplication}" + + " | " + + f"{plugin.active_commit.message}" + ) + else: # overwrite active commit if plugin.active_commit is None: + # print("set index to 1st") + plugin.active_commit = branch.commits.items[0] + else: + plugin.active_commit = None + + self.commitDropdown.setItemText(0, "Latest commit from this branch") + # enable or disable web view button + # print("_________ENABLE OR DISABLE") + # print(plugin.active_commit) + # print(f"CURRENT TEXT2: {self.streamBranchDropdown.currentText()}") + if plugin.active_commit is not None and plugin.btnAction == 1: + self.commit_web_view.setEnabled(True) + else: + self.commit_web_view.setEnabled(False) + except Exception as e: + logToUser(str(e), level=2, func=inspect.stack()[0][3], plugin=self) def onStreamRemoveButtonClicked(self, plugin): - try: - #from ui.project_vars import set_project_streams - if not self: return - index = self.streamList.currentIndex() - if len(plugin.current_streams) > 0: plugin.current_streams.pop(index) - plugin.active_stream = None - self.streamBranchDropdown.clear() - self.commitDropdown.clear() - #self.streamIdField.setText("") - - #set_project_streams(plugin) - self.populateProjectStreams(plugin) - except Exception as e: - logToUser(str(e), level=2, func = inspect.stack()[0][3], plugin=self) + return + def cancelOperations(self): + return diff --git a/qt_ui/ui/mainWindow_main.ui b/qt_ui/ui/mainWindow_main.ui index e2308bb..ded9114 100644 --- a/qt_ui/ui/mainWindow_main.ui +++ b/qt_ui/ui/mainWindow_main.ui @@ -1,7 +1,7 @@ - SpeckleQArcGISDialog - + SpeckleGISDialog + 0 @@ -98,9 +98,21 @@ - - + + + + + + + + + + + + + + @@ -126,6 +138,15 @@ + + + + + true + + + + @@ -178,21 +199,49 @@ true - Set visible layers as selection + Save current layer selection - + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + + true + + + Set project center on Send/Receive + + + + + + + + Message - + Sent XXX objects from ArcGIS @@ -202,7 +251,7 @@ - + @@ -236,46 +285,8 @@ - - - - - Lat, Lon - - - - - - - - - 0.0 - - - - - - - 0.0 - - - - - - - - true - - - Set as a project center - - - - - - - + diff --git a/qt_ui/widget_dependencies_upgrade.py b/qt_ui/widget_dependencies_upgrade.py index 24911cb..11411e2 100644 --- a/qt_ui/widget_dependencies_upgrade.py +++ b/qt_ui/widget_dependencies_upgrade.py @@ -6,7 +6,7 @@ from specklepy.logging import metrics try: from specklepy_qt_ui.qt_ui.DataStorage import DataStorage -except ModuleNotFoundError: +except ModuleNotFoundError: from speckle.specklepy_qt_ui.qt_ui.DataStorage import DataStorage from PyQt5 import QtWidgets, uic, QtCore @@ -91,7 +91,11 @@ To do it manually, you can run 2 following commands from QGIS Plugins panel->Pyt def runSubprocess(self): import subprocess - from speckle.utils.utils import get_qgis_python_path as path + + try: + from speckle.utils.utils import get_qgis_python_path as path + except ModuleNotFoundError: + from speckle.speckle.utils.utils import get_qgis_python_path as path result1 = subprocess.run( [path(), "-m", "pip", "install", "requests==2.31.0"],