From dc70a093d752beea7af054fc34be792440c9c864 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:45:20 -0700 Subject: [PATCH 01/20] TMP DEBUG --- python/tank/authentication/site_info.py | 4 ++++ python/tank/log.py | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/python/tank/authentication/site_info.py b/python/tank/authentication/site_info.py index 1689c1b288..28a4c5b80e 100644 --- a/python/tank/authentication/site_info.py +++ b/python/tank/authentication/site_info.py @@ -40,6 +40,10 @@ def _get_site_infos(url, http_proxy=None): :returns: A dictionary with the site infos. """ + # logger.info("Sleep for 10s") + # time.sleep(30) + # logger.info("done sleeping") + # Checks if the information is in the cache, is missing or out of date. if url not in INFOS_CACHE or ( (time.time() - INFOS_CACHE[url][0]) > INFOS_CACHE_TIMEOUT diff --git a/python/tank/log.py b/python/tank/log.py index a3c6486367..d8ce0630e9 100644 --- a/python/tank/log.py +++ b/python/tank/log.py @@ -425,6 +425,14 @@ def __new__(cls, *args, **kwargs): return cls.__instance + @classmethod + def reset_logger(cls): + if not cls.__instance: + return + + del cls.__instance + cls.__instance = None + @staticmethod def get_logger(log_name): """ @@ -677,7 +685,7 @@ def initialize_custom_handler(self, handler=None): handler = logging.StreamHandler() # example: [DEBUG tank.log] message message - formatter = logging.Formatter("[%(levelname)s %(name)s] %(message)s") + formatter = logging.Formatter("%(asctime)s [%(levelname)s] %(message)s") handler.setFormatter(formatter) From 517d3d6fd1cbc43e2b7e5d1f8c5bc96a529c2a13 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:46:02 -0700 Subject: [PATCH 02/20] Add cache to session data to reduce the amount of logs and file loading --- python/tank/authentication/session_cache.py | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/python/tank/authentication/session_cache.py b/python/tank/authentication/session_cache.py index a96e8d40f2..88d95d80db 100644 --- a/python/tank/authentication/session_cache.py +++ b/python/tank/authentication/session_cache.py @@ -22,6 +22,7 @@ from __future__ import with_statement import os import socket +import time from tank_vendor.shotgun_api3 import ( Shotgun, AuthenticationFault, @@ -148,7 +149,33 @@ def _ensure_folder_for_file(filepath): return filepath +_YML_CONMTENT_CACHE = {} +_YML_CONMTENT_CACHE_TIMEOUT = 300 # 5 minutes + def _try_load_yaml_file(file_path): + global _YML_CONMTENT_CACHE + global _YML_CONMTENT_CACHE_TIMEOUT + + cache = _YML_CONMTENT_CACHE.get(file_path, None) + if cache and (time.time() - _YML_CONMTENT_CACHE_TIMEOUT > cache.get("timeout", 0)): + try: + del(_YML_CONMTENT_CACHE[file_path]) + except KeyError: + pass + + cache = None + + if not cache: + cache = { + "timeout": time.time(), + "content": _try_load_yaml_file_real(file_path), + } + _YML_CONMTENT_CACHE[file_path] = cache + + return cache["content"] + + +def _try_load_yaml_file_real(file_path): """ Loads a yaml file. @@ -306,6 +333,11 @@ def _write_yaml_file(file_path, users_data): finally: os.umask(old_umask) + global _YML_CONMTENT_CACHE + try: + del(_YML_CONMTENT_CACHE[file_path]) + except KeyError: + pass def delete_session_data(host, login): """ From 4500beb323a1fe9863580024800c81fafc75a8d6 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:46:39 -0700 Subject: [PATCH 03/20] Improve docs and logs of get_session_data method --- python/tank/authentication/session_cache.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/python/tank/authentication/session_cache.py b/python/tank/authentication/session_cache.py index 88d95d80db..348431c016 100644 --- a/python/tank/authentication/session_cache.py +++ b/python/tank/authentication/session_cache.py @@ -369,10 +369,10 @@ def delete_session_data(host, login): def get_session_data(base_url, login): """ - Returns the cached login info if found. + Returns the cached authentication info if found. - :param base_url: The site to look for the login information. - :param login: The user we want the login information for. + :param base_url: The site to look for the authentication information. + :param login: The user we want the authentication information for. :returns: Returns a dictionary with keys login and session_token or None """ @@ -400,10 +400,11 @@ def get_session_data(base_url, login): session_data[_SESSION_METADATA] = user[_SESSION_METADATA] return session_data - logger.debug("No cached user found for %s" % login) except Exception: logger.exception("Exception thrown while loading cached session info.") - return None + return + + logger.debug(f"No authentication info found for user {login}") def cache_session_data(host, login, session_token, session_metadata=None): From 646986f13dae27d647fcce680062b85b0632df20 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:47:08 -0700 Subject: [PATCH 04/20] Remove duplicated log --- python/tank/authentication/session_cache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/python/tank/authentication/session_cache.py b/python/tank/authentication/session_cache.py index 348431c016..2b2424a533 100644 --- a/python/tank/authentication/session_cache.py +++ b/python/tank/authentication/session_cache.py @@ -572,7 +572,6 @@ def get_recent_users(site): """ info_path = _get_site_authentication_file_location(site) document = _try_load_site_authentication_file(info_path) - logger.debug("Recent users are: %s", document[_RECENT_USERS]) return _get_recent_items(document, _RECENT_USERS, _CURRENT_USER, "users") From 1a5cf2dc388d314fde72bac385a80409c64a29ff Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:49:50 -0700 Subject: [PATCH 05/20] Move thread wait out of the __init__ --- python/tank/authentication/login_dialog.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index fd24529620..2d698ed50b 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -312,14 +312,6 @@ def __init__( self._query_task.finished.connect(self._toggle_web) self._update_ui_according_to_site_support() - # We want to wait until we know what is supported by the site, to avoid - # flickering GUI. - if not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): - logger.warning( - "Timed out awaiting requesting information: %s" - % self._get_current_site() - ) - # Initialize exit confirm message box self.confirm_box = QtGui.QMessageBox( QtGui.QMessageBox.Question, @@ -693,6 +685,14 @@ def result(self): return self._sso_saml2.get_session_data() + # We want to wait until we know what is supported by the site, to avoid + # flickering GUI. + if not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): + logger.warning( + "Timed out awaiting requesting information: %s" + % self._get_current_site() + ) + res = self.exec_() if res != QtGui.QDialog.Accepted: return From 29f732ee94f3ba20709892749984cdf9d357a7dd Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:52:05 -0700 Subject: [PATCH 06/20] No period at the end of a log line --- python/tank/authentication/shotgun_authenticator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/tank/authentication/shotgun_authenticator.py b/python/tank/authentication/shotgun_authenticator.py index 7987650d21..9a400a5de5 100644 --- a/python/tank/authentication/shotgun_authenticator.py +++ b/python/tank/authentication/shotgun_authenticator.py @@ -240,8 +240,8 @@ def get_default_user(self): # There is no default user. if not credentials: - logger.debug("No default user found.") - return None + logger.debug("No default user found") + return # If this looks like an api user, delegate to create_script_user. # If some of the arguments are missing, don't worry, create_script_user From 8d9f9b983591af61b852474b8f1e6344fc57a7dc Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 09:29:22 -0700 Subject: [PATCH 07/20] Remove useless logs --- .../tank/authentication/sso_saml2/core/sso_saml2_core.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/python/tank/authentication/sso_saml2/core/sso_saml2_core.py b/python/tank/authentication/sso_saml2/core/sso_saml2_core.py index e148aa7d91..dfe524257e 100644 --- a/python/tank/authentication/sso_saml2/core/sso_saml2_core.py +++ b/python/tank/authentication/sso_saml2/core/sso_saml2_core.py @@ -222,17 +222,10 @@ def __init__(self, profile, parent, developer_mode=False): """ Class Constructor. """ - get_logger().debug("TKWebPageQtWebEngine.__init__") - super(TKWebPageQtWebEngine, self).__init__(profile, parent) + super().__init__(profile, parent) self._profile = profile self._developer_mode = developer_mode - def __del__(self): - """ - Class Destructor. - """ - get_logger().debug("TKWebPageQtWebEngine.__del__") - def acceptNavigationRequest(self, url, n_type, is_mainframe): """ Overloaded method, to properly control the behaviour of clicking on From de8ad90f18d2f0403a0f4c36d7014d566790519c Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:55:38 -0700 Subject: [PATCH 08/20] Cache DefaultManager host and login to prevent multiple call/logs --- .../tank/authentication/defaults_manager.py | 39 ++++++++++++------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/python/tank/authentication/defaults_manager.py b/python/tank/authentication/defaults_manager.py index 886b5c11cb..b9b7cc00bd 100644 --- a/python/tank/authentication/defaults_manager.py +++ b/python/tank/authentication/defaults_manager.py @@ -39,6 +39,8 @@ def __init__(self, fixed_host=None): self._user_settings = UserSettings() self._system_settings = SystemSettings() self._fixed_host = fixed_host + self._host = False # current value might be None + self._login = False # current value might be None def is_host_fixed(self): """ @@ -78,11 +80,15 @@ def get_host(self): :returns: A string containing the default host name. """ - return ( - self._fixed_host - or session_cache.get_current_host() - or self._user_settings.default_site - ) + + if self._host is False: + self._host = ( + self._fixed_host + or session_cache.get_current_host() + or self._user_settings.default_site + ) + + return self._host def set_host(self, host): """ @@ -94,6 +100,9 @@ def set_host(self, host): if self.is_host_fixed(): return session_cache.set_current_host(host) + self._host = host + # Reset login + self._login = False def get_http_proxy(self): """ @@ -130,13 +139,17 @@ def get_login(self): """ # Make sure there is a current host. There could be none if no-one has # logged in with Toolkit yet. - if self.get_host(): - return ( - session_cache.get_current_user(self.get_host()) - or self._user_settings.default_login - ) - else: - return self._user_settings.default_login + + if self._login is False: + if self.get_host(): + self._login = ( + session_cache.get_current_user(self.get_host()) + or self._user_settings.default_login + ) + else: + self._login = self._user_settings.default_login + + return self._login def get_user_credentials(self): """ @@ -163,8 +176,6 @@ def get_user_credentials(self): """ if self.get_host() and self.get_login(): return session_cache.get_session_data(self.get_host(), self.get_login()) - else: - return None def set_login(self, login): """ From 7d8d62524713e0b3419dd7e168e6e75296daa80a Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:57:12 -0700 Subject: [PATCH 09/20] Set the timer interval once and for all --- python/tank/authentication/login_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index 2d698ed50b..0746f286b2 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -206,6 +206,7 @@ def __init__( # This is to make the UX smoother, as we do not check after each character # typed, but instead wait for a period of inactivity from the user. self._url_changed_timer = QtCore.QTimer(self) + self._url_changed_timer.setInterval(USER_INPUT_DELAY_BEFORE_SITE_INFO_REQUEST) self._url_changed_timer.setSingleShot(True) self._url_changed_timer.timeout.connect( self._update_ui_according_to_site_support @@ -406,7 +407,7 @@ def _site_url_changing(self, text): """ Starts a timer to wait until the user stops entering the URL . """ - self._url_changed_timer.start(USER_INPUT_DELAY_BEFORE_SITE_INFO_REQUEST) + self._url_changed_timer.start() def _on_site_changed(self): """ From a7da0e6f4aca980bd046759e86dece13499e69d8 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Fri, 20 Jun 2025 12:50:27 -0700 Subject: [PATCH 10/20] Fixup missing otherwise, create this change: -from . import QtCore +from tank.platform.qt import QtCore --- build_resources.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/build_resources.yml b/build_resources.yml index af26f47dab..c3cb449a34 100644 --- a/build_resources.yml +++ b/build_resources.yml @@ -22,3 +22,4 @@ - ui_busy_dialog res_files: - resources + import_pattern: . From 42d73db9861f2ff38d37f7c79e97f85d0430c26b Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Fri, 20 Jun 2025 13:21:46 -0700 Subject: [PATCH 11/20] Fixup verticalLayout_2 name was used twice uic script said: > Warning: The name 'verticalLayout_2' (QVBoxLayout) is already in > use, defaulting to 'verticalLayout_21'. --- .../authentication/resources/login_dialog.ui | 2 +- python/tank/authentication/ui/login_dialog.py | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/python/tank/authentication/resources/login_dialog.ui b/python/tank/authentication/resources/login_dialog.ui index b149820757..0e1e94dd20 100644 --- a/python/tank/authentication/resources/login_dialog.ui +++ b/python/tank/authentication/resources/login_dialog.ui @@ -148,7 +148,7 @@ QPushButton.main:pressed true - + 20 diff --git a/python/tank/authentication/ui/login_dialog.py b/python/tank/authentication/ui/login_dialog.py index 8cf2f9fcc8..7bb3bc2da7 100644 --- a/python/tank/authentication/ui/login_dialog.py +++ b/python/tank/authentication/ui/login_dialog.py @@ -153,9 +153,9 @@ def setupUi(self, LoginDialog): "}\n" "") LoginDialog.setModal(True) - self.verticalLayout_2 = QVBoxLayout(LoginDialog) - self.verticalLayout_2.setContentsMargins(20, 20, 20, 20) - self.verticalLayout_2.setObjectName(u"verticalLayout_2") + self.verticalLayout_asl = QVBoxLayout(LoginDialog) + self.verticalLayout_asl.setContentsMargins(20, 20, 20, 20) + self.verticalLayout_asl.setObjectName(u"verticalLayout_asl") self.horizontalLayout = QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName(u"horizontalLayout") @@ -174,7 +174,7 @@ def setupUi(self, LoginDialog): self.horizontalLayout.addWidget(self.logo) - self.verticalLayout_2.addLayout(self.horizontalLayout) + self.verticalLayout_asl.addLayout(self.horizontalLayout) self.stackedWidget = QStackedWidget(LoginDialog) self.stackedWidget.setObjectName(u"stackedWidget") @@ -429,9 +429,9 @@ def setupUi(self, LoginDialog): self.stackedWidget.addWidget(self.backup_page) self.asl_page = QWidget() self.asl_page.setObjectName(u"asl_page") - self.verticalLayout_21 = QVBoxLayout(self.asl_page) - self.verticalLayout_21.setContentsMargins(20, 20, 20, 20) - self.verticalLayout_21.setObjectName(u"verticalLayout_21") + self.verticalLayout_2 = QVBoxLayout(self.asl_page) + self.verticalLayout_2.setContentsMargins(20, 20, 20, 20) + self.verticalLayout_2.setObjectName(u"verticalLayout_2") self.asl_msg = QLabel(self.asl_page) self.asl_msg.setObjectName(u"asl_msg") sizePolicy1.setHeightForWidth(self.asl_msg.sizePolicy().hasHeightForWidth()) @@ -440,31 +440,31 @@ def setupUi(self, LoginDialog): self.asl_msg.setAlignment(Qt.AlignCenter) self.asl_msg.setWordWrap(True) - self.verticalLayout_21.addWidget(self.asl_msg) + self.verticalLayout_2.addWidget(self.asl_msg) self.asl_msg_back = QLabel(self.asl_page) self.asl_msg_back.setObjectName(u"asl_msg_back") self.asl_msg_back.setAlignment(Qt.AlignCenter) self.asl_msg_back.setWordWrap(True) - self.verticalLayout_21.addWidget(self.asl_msg_back) + self.verticalLayout_2.addWidget(self.asl_msg_back) self.asl_spacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding) - self.verticalLayout_21.addItem(self.asl_spacer) + self.verticalLayout_2.addItem(self.asl_spacer) self.asl_msg_help = QLabel(self.asl_page) self.asl_msg_help.setObjectName(u"asl_msg_help") self.asl_msg_help.setAlignment(Qt.AlignCenter) self.asl_msg_help.setWordWrap(True) - self.verticalLayout_21.addWidget(self.asl_msg_help) + self.verticalLayout_2.addWidget(self.asl_msg_help) self.stackedWidget.addWidget(self.asl_page) - self.verticalLayout_2.addWidget(self.stackedWidget) + self.verticalLayout_asl.addWidget(self.stackedWidget) - self.verticalLayout_2.setStretch(0, 1) + self.verticalLayout_asl.setStretch(0, 1) self.retranslateUi(LoginDialog) From 30854fc4b32f87bc848eb8989dc4e8beb302c1b4 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Thu, 19 Jun 2025 07:47:47 -0700 Subject: [PATCH 12/20] WIP --- python/tank/authentication/login_dialog.py | 39 +++++++++++++------ .../authentication/shotgun_authenticator.py | 2 + 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index 0746f286b2..c29f2bb65a 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -208,9 +208,7 @@ def __init__( self._url_changed_timer = QtCore.QTimer(self) self._url_changed_timer.setInterval(USER_INPUT_DELAY_BEFORE_SITE_INFO_REQUEST) self._url_changed_timer.setSingleShot(True) - self._url_changed_timer.timeout.connect( - self._update_ui_according_to_site_support - ) + self._url_changed_timer.timeout.connect(self._on_site_changed) # If the host is fixed, disable the site textbox. if fixed_host: @@ -311,7 +309,8 @@ def __init__( self._query_task = QuerySiteAndUpdateUITask(self, self.site_info, http_proxy) self._query_task.finished.connect(self._toggle_web) - self._update_ui_according_to_site_support() + + self._on_site_changed() # trigger the first site info request # Initialize exit confirm message box self.confirm_box = QtGui.QMessageBox( @@ -383,9 +382,7 @@ def _get_current_site(self): :returns: The site to connect to. """ - return sgutils.ensure_str( - connection.sanitize_url(self.ui.site.currentText().strip()) - ) + return self.host_selected def _get_current_user(self): """ @@ -399,7 +396,10 @@ def _update_ui_according_to_site_support(self): """ Updates the GUI according to the site's information, hiding or showing the username/password fields. + TODO the name and description of this method are not accurate!!!!! """ + + logger.debug("_update_ui_according_to_site_support") self._query_task.url_to_test = self._get_current_site() self._query_task.start() @@ -408,14 +408,24 @@ def _site_url_changing(self, text): Starts a timer to wait until the user stops entering the URL . """ self._url_changed_timer.start() + # If the timer is already running, it will be stopped and restarted def _on_site_changed(self): """ Called when the user is done editing the site. It will refresh the list of recent users. """ - self.ui.login.clear() - self._populate_user_dropdown(self._get_current_site()) + + host_selected = connection.sanitize_url(self.ui.site.currentText()) + if host_selected == self.host_selected: + logger.debug(f"_on_site_changed - site has not changed: {host_selected}") + # No change, nothing to do. + return + + self.host_selected = host_selected + + self.ui.login.clear() ## TODO do we really want to do that here? + self._populate_user_dropdown(self._get_current_site()) # same TOTO self._update_ui_according_to_site_support() def _populate_user_dropdown(self, site): @@ -467,7 +477,13 @@ def _toggle_web(self, method_selected=None): """ site = self._query_task.url_to_test - self.method_selected_user = None + if self._get_current_site() != site: + logger.debug("_toggle_web - site-info thread finished too late, we already selected another host") + # the thread finished too late, We already selected another host + return + # Careful, this method is called from a lot different use cases.......!!!!!! + + # self.method_selected_user = None ## WHY ????? # We only update the GUI if there was a change between to mode we # are showing and what was detected on the potential target site. @@ -533,14 +549,13 @@ def _toggle_web(self, method_selected=None): else: method_selected = auth_constants.METHOD_BASIC - if site == self.host_selected and method_selected == self.method_selected: + if method_selected == self.method_selected: # We don't want to go further if the UI is already configured for # this site and this mode. # This prevents erasing any error message when various events would # toggle this method return - self.host_selected = site self.method_selected = method_selected # if we are switching from one mode (using the web) to another (not using diff --git a/python/tank/authentication/shotgun_authenticator.py b/python/tank/authentication/shotgun_authenticator.py index 9a400a5de5..559be95207 100644 --- a/python/tank/authentication/shotgun_authenticator.py +++ b/python/tank/authentication/shotgun_authenticator.py @@ -232,6 +232,8 @@ def get_default_http_proxy(self): def get_default_user(self): """ Returns the default user from the defaults manager. + TODO this method's name and description is not accurate. This method tries to return the current user with credentials. ... + This is misleading...... :returns: A :class:`ShotgunUser` derived instance if available, None otherwise. """ From 2cdd77e8a6e1c031dbc06be031c7545a95d166e1 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Fri, 20 Jun 2025 12:29:19 -0700 Subject: [PATCH 13/20] Spinner step1: add file --- .../tank/authentication/resources/resources.qrc | 3 +++ .../authentication/resources/spinning_wheel.gif | Bin 0 -> 12185 bytes 2 files changed, 3 insertions(+) create mode 100644 python/tank/authentication/resources/spinning_wheel.gif diff --git a/python/tank/authentication/resources/resources.qrc b/python/tank/authentication/resources/resources.qrc index 6e764ca76b..b67f9ba4d7 100644 --- a/python/tank/authentication/resources/resources.qrc +++ b/python/tank/authentication/resources/resources.qrc @@ -11,4 +11,7 @@ google_authenticator.png + + spinning_wheel.gif + diff --git a/python/tank/authentication/resources/spinning_wheel.gif b/python/tank/authentication/resources/spinning_wheel.gif new file mode 100644 index 0000000000000000000000000000000000000000..b74a22c4a8aff744659dddf99024b2fbf1c1399f GIT binary patch literal 12185 zcmeI&c~sAN-^X!Uq%x$^GGhr*AxkA;&|b1uR1zX3YZ4~w)UPCIUz8LrzxEJ=R!U`Q zBUuIwvX{YxS(tmq+|OTM*E09ab)D;+>$?8AXXcza*EuuS%$#Z7ugClK{(RX?w>Fu$ zRI8g-xAon+4Spxny<0cmZlk*Ko8*s$@V}pb{`t0T+qQ1qx_|%vsHmujh=|?0cW>FU zWy6LID_5@c^74v_iHVGi+`fIgx3{;ar)OefVsv!$XPDz|GBWb>^W)>=U0hro931A(o$KoAnwy*JOHE5dL&N3Emv7&`efRF&=g*&i`|Y>-`uaLL zIwMDp96NUG=+UE_nwnZ$TkqVt)7IAZ;K75Aj*b^EUJM^TTu)DL#E22su3fuv7cai)d=>HLrSHmZ~b)nbG&dXIj7h`s*=c#xTeeCQM-VnL)<$<;#~0 zCi6>H7;^@H;>3x}?5kI=7*EEQ0Ve^*#>NaR18!ns!qkr%HEQzY$^Y`m{`r6Whd+Uy zzjV`+?{8bw7V@#I$_O{xq8{cUN^<#g?TgNbh|ENm+9t1tn5p!U^XgS!`{y zaCcBhZqBXBpQp0Sv#e#_D39dljEu`0CD# z^FH^=LRDIgYHs$bHS-zbv!>Bx*`o#}y~%1lb?>JI#tg2Up1VJ7|0P+u9cuoXZ%pfD zy4R^C>Bg8|FYqbfnL5|_+3n)_+wQh+)9P$1U8^=>-6vZ%JjjtBXfyh=iEpk|CuE+P zJ+}NoN0^F8I!Z(7gA0+rb^)2mv#=w`j9`IeHHgLJRtO;(E+{B~00be8gp*(sT@Zl} zb`VKEfBrnOUF2N@DY(T_mtZNJk3IZu|OOByINbjain}{VrWp}Jww+>K* z7{iSq3uFj;0b+myP{{4WhYv~elP6Euj7yg;C9Wiy#4cE{VDaL`IM|XXV`pb4`0Er7}QnFnxG)qfdSXSlUdEc~;_7LS1 z+n6+|KC<=2r z-VNU$rzJmluKd99Wyiei(j(JW%`)lNVV>@K=;f#R_N%Xj&d4=4n*7D#)?(Sji>Z?n zA4^IC}IbZBqzrOG`^bT|;$sH4PvoC57(7#wFKifGwvhP9Of_i`=~2jLZzQK_LlE zaj{s0FbF`#Z{EC#Gt?Bi2x?&zn^>G9PD@LJ9YHFLLNlmAp{Gxu3Qdw|6Mu>hjRcx@ z?b?L^gdJ@JrqF<}LlY381jUj{L~@Ba0r&IsgB>3qA0kg91q9@Q9ZC-=1_uuhPil&n zrx!&BqsTZi0Ep0j5oY1Sg+PQ(7zGYkfh0JCBv1ocXhv5+63_q&sG*}^5k}E

2TO z>FG6n`gF=U+5i`Zzzbysk^YUi{DJ;bWSG*`7y_lcWa!kY*)5|pb8KidRwyajiCsD~ zqQj+!&FG4| z9=*A?eFr-QD-T#@bbp4(Da~t^$w_bP!S2NdDL-BhTM)f`Q1VA3Br-^!yJ$7ex!0oT zrLUizjCDS8uX0)EvjP2k9JybkbW7Lk&DiLNrkd&_FTK?;s5!SnW7{OZ;pqnh6ZBPT z3m0gu2n_D=>|L+2*X@m26ApcEse3Z#dZBFDz(vt-p5M-O)=_P0{=u~EtY41N)Xwl1 zttG7nHb3_-+Yov+Pd)rhg`wHg)}xg@ts+eZzYFjFmvbWb*ErCXi7f)K_whjpK1vAr zC5EIAF3^K83gIQxSW#SBDs*B=wJ0qm-~<*@*uP|!?N6@&3KB=Sh$KWDJ9aELFE1xA zk0g>!mNo0Xp`j5U2srJ6asoBkIXU92EaFLo+3Ce4C5I0mmJ}OtW@c9AjA{f;hDQ8#Z4h@$~Y-D|F%?i8+Ad;OOAs=t%AP1Lgmp{;Pt6 z{cmtk(FO8}LmIWgD$;VkLz~qgUB`8*H(zU8_LaA4DfEuhnkgD0 zdJsSH`0_dPrfzS1t2fqLs_#hqrW0@N-5u1Yx@oH_jDK8HFg14Uqe&>%}AZ%9P;U012fwq*-5@bls2sjOn9u?IJ0y1nt2DkGtHNmr>zSunw6P6 zrO1DwZB~MzPvnp9e446PJKM#G&Z0(TOM(LFfUSH*W?7 znhM;QTbNr|O3nt>Ke;8SrsZO!R=BVB*ejqYd(p0j7wvb^S1S9i@Z zbd}Zoe(=>K@2mOx-kFCN_FZRYoiroyNNc&Tp;K~su}|>H0BMV02U!Kx(3lbPrR2|r zEIl2gJZ7Ax*``?~e=ie5c`|G+>4;tv8WC(?uuKtV9cyKqR8P;w0&6bK?r-pMtqU4R7*=m$VS zpWrrN@A8}m7i5+-%<^WVqYbqKRWJuJgawQM)CdZ}mtTGfLR=< zj8#oMSCTo~O*C{A0L@f|N7>=Cm>?}Yck|b9MHEsYx(!L~)EBwJF z;3Cu{S8`o+t#A@e84nK0EXZIjNl9|p;khC0)En; zkO6Aoi(6%xhX-8*Nf0SxUk0x&oJrV(Y*+zx+-XqrJ7M_q5B=sR@Ye$);O~6sr#m*Z zXsK*y-(eqD*cORwJ{%ZnHj!FW_Vn+N_@HO83Ll1B_KG>H*`D2d{a&w5?XE<5Rba65q2IL~<=LZq3n}BBv1x z)m*yyKDl^6wzq+{(&hBa(iZB&{ayW5%?auuZ?rLVliAHa6a3B}-d3!6Z``gvgT#B% zR}J-sa6mu+5yb@%IeGFV zvVabGCiX;{-XLiVadGGXA|#o=JByrr?%2MAxRP^X92*mxl9VFQP+C?71bk|tj=%~M z(S)cV@PP|to0}ssK7QL)Nt2-KfDTgpX0ZSK1OI`a06KI%OuC2k5*{YIOCBaCt>C`d{A8l^ee?hD`GfHdzq-A=|V$=AKowda)-*{BV%Q&03 zr27qzhYj9vuFOIhErOK zY9sH~>*tRPy;J_#X^CmAZm5Nt@pcVg)lgX%Rj0h#R}V#dH5KP& z4o_DeCZoSZO?UrTvrDN1ZXFxidHHE`EEREo$2IVJ*2iCR@=1p z`dPX6nv30+@4D*DN1Vf#T?+16#J3O>W8Ylb1_bRAk<1@K+uF&*w~XD>8&+U4z0-!dcy1=%kvr4nP4H zx{T0nfB@sr2X2Hf7ce1w$RM#qnygY}==%hc&H^;JgEA<<>K1l5E)aXT;qy8Tg_QDo zNkl0QU<50?ok|?4<6CGW*A&wfFubqr%yukfjH#+a@YpRdg`PRSJ z@|aj1_3h#QZArDVKffKEbgS1{{e5PN{T;Kzt4w@l!Uj$m`{qfor;Fk!l>;qnZl5$f z?)1^Nw&%q)i@tfOzwpGu8}*4QpPn%&Ue$3m)j8^Y@|HK%@%}-w&MwL zktfcaM^tQ9GLfX76H>In6H-qA(Svj&1G$45q0^uWp_Uv6Nl5^LC$t&Xe0*FyX=Sft z0eyw|qYW`9#rQzXi7HtJ0pbi1*a9*%9(Q$4J92;t5 zr9Lfep7twoO`x80WW2Ysmu;ww@+sAJdw078snx35rbbU2W5;;(E(}Z$X%4j7vudWl zVJ^L4?~t+eu_I>Y%+j=eT;G|0HQauX!IY7bmr0pGt_o*yRJs3~mOJGw7t|^PeyK)= zWwA9Y2CiFBVWs_`R!MV=SJI=Io?}K1n=snD)bOmKo$~v47ITmH5haZL+)3HB?+{BV z8y}j4pZSf$OD+sdU2kmBe7Ijz;OPS!7Y<94e>TwM_FKC)ncXUDMyr%DU~nA}U(6uuBpW|qgUAXmXq@5% z8aChrKS(F3CA)Nfp5*8tP=O^Ri3Gy~?HvQy_Gm<{Ko_bC^#l)C;3S(G4>sH~lxmK4 zK@nojCWjy5&gv%j;6cJjIY&9M=9!Oo3J`)K03iBkggwNaUK5j?Oqyx${GN2+0McL# ziRX=yrx1z>?;~t-7!bBP(tP*bchq>{T>yS)H?$tQ3Blz%2~VJT1$pFHP=V(Xp|c1G zffH!);WsBY7dc>nl=BGkS2=(Oleo5k0YYGa69YnM5NN@#(ZeU<;L6FD92)>rSyjbr z3y&{cXMn-mF`sd$EOZ|vqkZ6(r2bsGjA8%vH2pvN2LQuF2@LK5-MT8n*Z`>zC3$0S zccsB*vVDg3PO&|felAeKW%;#>donNV)tJ66#os~8I%b6P=0_Wqme+@xC~P?45_h#B zc$C5ejbz8{s)HIf8Ts)QvgLF4<2ANl4H%Kte!aomZb@lCL}>Jk*l+IE_^6DGbTPFI4D}ciHTYIZl}D(@ zsOcf?H(x#8wXcWQUALm+o}$Fj!%nEWo_JIzt$%1>lI!@KX@?gStL0AI&=Gu6_Wiq> z1DnimDg`O(EZ$yh_qajs+2$jCe_Z>d$wSR{(RzQg8y5>&KN{Gcn)d44V|AUGN5}mG zA@G32V*nML++zb6kY+MX;)Qa6AAF)C`Xrb)G};4@u%z*TbP{EH1NjCOY~Y=rxRYi+ zuz>-01qhLH0!_H76xcvVp+*R4W*@U|sRa~$5aL%d(m!|}6zri-2)7L~aaUjoxsQ&C z0R}unFBroo4B%0YO8_~rgzpi2T)-hd{}X+(4heWeQz7KIg-p=J`tOP|Va|mmtSTxh zBDJKSa2FhrdAb|k@ zAan?ljT5*c@PlVKLAYUn{(%I75S&bSBm9!7@~##_`M@E#!a`~aJW!1IRg-7|9>N>X z|Dyo^*Z)=iB1GHYKS*|c)J*HyS4nP1QpI%Atxl^`j{;FFIiDQ(@0=j?wqSaaQQS-WP z$j$7gyU}6lhU>n1lK#LfR^9Gn+fv`^i~A?m#l+2R@d`|x(nn_2dZU%I6Gq4vE>CuU zDq5vnc2NI7T*uW@hA*E?i8fxJP_swzryXu3DR=7=zqFoO=Dz0Zs}@c={;BSpYwq;b Lk?C~^7$*NOUXI;w literal 0 HcmV?d00001 From f1139777944f395915cc22acd569f035ed54d0b8 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Fri, 20 Jun 2025 13:29:29 -0700 Subject: [PATCH 14/20] Spinner step2: rebuild resource file --- python/tank/authentication/ui/resources_rc.py | 793 +++++++++++++++++- 1 file changed, 782 insertions(+), 11 deletions(-) diff --git a/python/tank/authentication/ui/resources_rc.py b/python/tank/authentication/ui/resources_rc.py index 95c0853167..979110c25d 100644 --- a/python/tank/authentication/ui/resources_rc.py +++ b/python/tank/authentication/ui/resources_rc.py @@ -6,6 +6,770 @@ from .qt_abstraction import QtCore qt_resource_data = b"\ +\x00\x00/\x99\ +G\ +IF89a \x00 \x00p\x00\x00!\xf9\x04\x01\ +\x00\x00q\x00,\x00\x00\x00\x00 \x00 \x00\x86\x00\x00\ +\x00\x7f\x7f\x7f{{{zzz\x99\x99\x99\x8f\x8f\x8f\ +\x8d\x8d\x8d\x87\x87\x87yyyrrriiif\ +ff\x91\x91\x91\x8e\x8e\x8e|||hhhee\ +e\x97\x97\x97\x90\x90\x90wwwgggddd\ +ccc\x92\x92\x92\xb2\xb2\xb2\xa5\xa5\xa5\xa3\xa3\xa3\x96\ +\x96\x96\x8c\x8c\x8clllbbbUUUTT\ +T\xa6\xa6\xa6\xa4\xa5\xa4\xff\xff\xff\x95\x95\x95\x91\x90\x90\ +\x8b\x8b\x8b}}}mmm___WWWS\ +SS\xa4\xa4\xa4\xa2\xa2\xa2\xaa\xaa\xaa\x94\x94\x94XX\ +XRRRPPPZZZ\xa8\xa8\xa8VVV\ +\xb6\xb6\xb6QQQMMM333OOO\xb8\ +\xb8\xb8\xb7\xb7\xb7\xcc\xcc\xccCCC\xbb\xbb\xbb\xbc\xbc\ +\xbc\xbf\xbf\xbfBBB???@@@\xb4\xb4\xb4\ +\xc3\xc3\xc3$$$KKK\xca\xca\xca\xca\xc9\xcaH\ +HH555\xc8\xc8\xc8\xce\xce\xce\xd0\xd0\xd011\ +1222444\xd2\xc3\xd2\xcb\xcb\xcb\xcf\xcf\xcf\ +\xda\xda\xda\xde\xde\xde\xec\xec\xec\xf4\xf4\xf4---'\ +''+++000...\xcd\xcd\xcd\xdb\xdb\ +\xdb\xdd\xdd\xdd\xe0\xe0\xe0\xe2\xe2\xe2\xe9\xe9\xe9\xed\xed\xed\ +&&&)))***\xd4\xd4\xd4\xd6\xd6\xd6\xdf\ +\xdf\xdf\xe1\xe1\xe1\xea\xea\xea(((\xdc\xdc\xdc\xf0\xf0\ +\xf0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\xb9\ +\x80q\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\ +\x90\x87\x13\x13\x91\x8d\x93\x95\x87\x04\x07\x83\x97\x82\x0b\x0b\ +\x98q\x07\x04\x83\x07\x94\x9e\xa0\x98\xa3\x9bq\x9d\x9f\xa1\ +\x82\x9a\x82\xae\xa9\x8f\x04.\x84\xa3\xa2\x94\xaf\x83\x1f\x1f\ +\x8c..\xb7\xb1\xac\xa8\x82\xbf\x1f>\x8e\xc2\xc2\x88\xc9\ +\xcb\x91\xc4\x87\xca\xb0\xd6\x83?\xd9\xda?\x8a>\xde\xdf\ +\xd1\x86\xdb\xda\x8a9>\xe6\xe7\xe1\xd7\xa1==\x889\ +9\x95\xed\xed\xef\xf0\xf1\x8d\xf3\xeeqaa\x84GG\ +\x82\xec\xddSDo\x10\xbf8e\xca\xc4\xf97\xc8^\ +\xa4\x83\x08\x15\xfe\x03\x18\x8a_\xbf\x88\x82\x18V\xbc\x88\ +1 \xc5Ja\x14\x0aJ8H\xe35\x92\xeb\x0a\xa1\ +L\xc9\xb2\xa5\xcb\x97\x86\x02\x01\x00!\xf9\x04\x01\x00\x00\ +\x94\x00,\x00\x00\x00\x00 \x00 \x00\x87\x00\x00\x00\x94\ +\x94\x94\x92\x92\x92\x8f\x90\x90\x90\x90\x90\xbf\xbf\xbf\x96\x96\ +\x96\x91\x91\x91\x8e\x8e\x8e\x8f\x8f\x8f\xff\xff\xff\xa9\xa9\xa9\ +\xa5\xa5\xa5\xa3\xa5\xa5\x93\x93\x97\x8d\x8d\x8d}}}{\ +{{|||\xa8\xa8\xa8\xa6\xa6\xa6\xa3\xa3\xa3\x95\x95\ +\x95\x8d\x90\x8d\x84\x84\x84zzz\xa7\xa7\xa7\x99\x99\x99\ +\x8c\x8c\x8c~~~xxx\xa3\xa4\xa4\xa2\xa2\xa2\x82\ +\x82\x82yyy\xa2\xa3\xa3\x9d\x9d\x9d\x97\x97\x97\x8d\x8e\ +\x8e\x7f\x7f\x7f\xba\xba\xba\xb9\xb9\xb9\xaa\xaa\xaa\xa4\xa4\xa4\ +\xa1\xa2\xa2\x9a\x9a\x9a\x81\x81\x81qqqjjjk\ +kk\xbb\xbb\xbb\xb7\xb7\xb7\xb2\xb2\xb2\x89\x89\x89\x86\x86\ +\x86lllhhhgggfff\xbd\xbd\xbd\ +\xbc\xbc\xbc\xb8\xb8\xb8\xae\xae\xae\x93\x93\x93\x80\x80\x80w\ +wweee\xbb\xbc\xbc\xaa\xad\xad\x9f\x9f\x9fpp\ +piiighhddd\xb6\xb6\xb6\x5c\x5c\x5c\ +\xbe\xbf\xbfccc\xc0\xc0\xc0```\xcd\xcd\xcd\xcc\ +\xcc\xcc\xcb\xcb\xcbZZZWWWXXX\xcc\xcb\ +\xcc\xcb\xca\xcbVVVUUUTTT\xce\xce\xce\ +\xcf\xcf\xcfRRRQQQSSS\xd1\xd1\xd1\xd3\ +\xd3\xd3\xd4\xd4\xd4\xd6\xd6\xd6OOOPPP\xdd\xdd\ +\xdd\xdc\xdc\xdc\xdf\xdf\xdfNNNHHHEEE\ +\xdb\xdb\xdbAAACCCDDD\xde\xde\xde\xe0\ +\xe0\xe0\xe4\xe4\xe4>>>@@@BBB\xe2\xe2\ +\xe2\xe8\xe8\xe8\xec\xec\xec\xeb\xeb\xeb///444\ +777???\xe1\xe1\xe1\xe9\xe9\xe9\xee\xee\xee*\ +**+++33322255599\ +9\xed\xed\xed\xf1\xf1\xf1((()))---\ +111666\xea\xea\xea...888\xf4\ +\xf4\xf4,,,;;;\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\x00)\ +\x09\x1cH\xb0\xe0\x86\x1a\x05\x13*\x5cH\xb0\x06B\x86\ +\x10\x07\xaaPQ\xb0\xc6\x86\x82A\x82D$8\xf1\x22\ +A\x13\x0f\x07f\x0c\xb9QE\x03\x8a\x03-6\xcc\xb8\ +\x91\xe3\x06\x94\x02\x1d\x12\xcc\xa8\xb1\xa5\xc4\x93\x037x\ +\xa4Ds\xa3\x8c\x84*^\xc6|X\x83eA\x1d\x05\ +e(\xfd)q\xa2\xc0\x83\x02{\x0e\xd4A\x15\xe9\xc0\ +\xa52`R\x0a\x9a\xd0\xa8\xc0\xaaT\x13be\xba\xb0\ +&%\xb0V\x17b\xdd\x88\xd6\xa7\xd2\x88`m\xca\x9d\ +K\x97a\x94\xbbx\xf1\xda\xcc\xc2\xb7o\xdf\xbcw\xc7\ +\xdc\xddK\xc6o\xdf\xba\x88\x13\xdb4\xc3\xd8\x0c\xc4A\ +o\x22ol\xcc8b\xe4\xcb\x10);\x16\x08(\xe1\ +\xa0\x81\x91!\xbfI\xa8\x99\xa0\x99\xce\x05\x07}\x16x\ +\x19\xf3@\xca\x04\x01\xc9\x16\x98(\x91@\xd5\xabo\xbb\ +~\x9dP6\xea\xda\xb7U{\xde\xe8{ \xf0\xe0\xb9\ +m\x167n\x1byr\xe2\xa7\x09\x1eG.w9\xf3\ +\xd4\xc2\x95\xa3\xbeN0\xbb\xe2\xe9\x8a\x13\x82\x03\x8f\x18\ +\x10\x00!\xf9\x04\x01\x00\x00\x8e\x00,\x00\x00\x00\x00 \ +\x00 \x00\x87\x00\x00\x00\xb4\xb4\xb4\xa7\xa7\xa7\xa5\xa5\xa5\ +\xa3\xa3\xa3\xcc\xcc\xcc\xff\xff\xff\xa9\xa9\xa9\xa6\xa6\xa6\xa4\ +\xa4\xa4\xaa\xaa\xaa\xbc\xbc\xbc\xb8\xb8\xb8\xb9\xb9\xb9\xab\xab\ +\xab\x95\x95\x95\x92\x92\x92\x93\x93\x93\xbd\xbd\xbd\xba\xba\xba\ +\xb7\xb7\xb7\x96\x96\x96\x8f\x8f\x8f\x8e\x8e\x8e\xbb\xbb\xbb\xc6\ +\xc6\xc6\x90\x90\x90\x8d\x8d\x8d\x91\x91\x91\xc3\xc3\xc3\x98\x98\ +\x98\xcd\xcd\xcd\xc9\xc9\xc9\xbf\xbf\xbf\xb6\xb6\xb6\xa1\xa1\xa1\ +\x88\x88\x88\x7f\x7f\x7f\xcb\xcb\xcb\xbe\xbe\xbe\xac\xac\xac\xa8\ +\xa8\xa8\x9b\x9b\x9b\x8c\x8c\x8c\x82\x82\x82|||{{\ +{zzz\xcf\xcf\xcf\xc1\xc1\xc1\x94\x97\x94\x8b\x8b\x8b\ +~~~yyy\xd0\xd0\xd0\xce\xce\xce\xcb\xca\xcb\xc0\ +\xc0\xc0\x9f\x9f\x9f\x87\x87\x87xxx\xcc\xc9\xcc\x83\x83\ +\x83}}}\xd1\xd1\xd1\xce\xcf\xcfwww\xd3\xd3\xd3\ +\xd4\xd4\xd4uuu\xe0\xe0\xe0\xdb\xdb\xdb\xda\xda\xdan\ +nnlll\xdd\xdd\xdd\xdc\xdc\xdcjjjhh\ +hgggiii\xdf\xdf\xdf\xe1\xe1\xe1fff\ +dddeee\xe2\xe2\xe2\xe3\xe3\xe3\xe4\xe4\xe4\xe6\ +\xe6\xe6bbbaaa\xe9\xe9\xe9\xe8\xe8\xe8\xee\xee\ +\xeeYYY^^^\xeb\xeb\xeb\xed\xed\xedSSS\ +TTTVVVXXX]]]\xea\xea\xea\xec\ +\xec\xecPPPQQQZZZUUU\xef\xef\ +\xef???''')))EEEFFF\ +DDDHHHRRR(((---*\ +**333777@@@AAA__\ +_\xfe\xfe\xfe000555222===\ +BBB+++444$$$CCCJ\ +JJ,,,;;;666888\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x08\xff\x00\x1d\x09\x1cH\xb0\xa0\x02\x05\x05\ +\x13*\x5cH\xd0\x03B\x86\x10\x07b\xc0`\xf0\xe1@\ +\x0f$\x22\x12\x9cH\x91\xa0\x02\x0f\x051f\xd4(\x90\ +\xa3G\x90\x03I`$)q\xa2G\x8b\x8eD\xb2l\ +\xd9\xd1\xd1\xc1\x8b$FB,\x90\xd0\xa4M\x94\x18Q\ +\x12\x14R\xb0\x80Q\x9e4\x05\xde\x8c\x99\x93 \x09!\ +P\x09\x1e5\xba\xb1fJ\xa1\x8e\xa0jM8\x15\xe9\ +B\xa1Z\x9f\xee<\xaaQk\xd4\x88K\xa8B4;\ +\xb3\xad\xdb\xb7\x05\x97\xc8\x9d;\x97\xe5\x94\xbbx\xf1\xd2\ +\xddk7o^\xb8\x80\x03G\xf4B\xd8\x0b\xc46\x88\ +\xdbh$\xbc\x84\xb0\xc6\xc4\x8a\x196.,\xf0\xd0\xa1\ +\x84t\x06\xd2\x81\x9cp\xb2\xe3\x81\x96\x05\xea\xd1\x9cY\ +`\x9b\xcd\x89\x09\x16^B\xf0\x90\x9e\xcb\x8e\xf4\x8c\x8e\ +M\xa7\xb4\xe9\xd4\xaa\x0b\xbev-zvm\xdb\x03#\ +C\xb4\x0c;\xf6l\xda\xc0I\xee&(\x9bt\xed\xb6\ +\x96\x8f\x1b'\xf8{ft\xdd\xd2\x1dUg\x19\x9ay\ +\xf6\xe7\x82\x9b\x0b\x05V(\x9ed@\x00!\xf9\x04\x01\ +\x00\x00\x9e\x00,\x00\x00\x00\x00 \x00 \x00\x87\x00\x00\ +\x00\xb2\xb2\xb2\xbc\xbc\xbc\xba\xba\xba\xda\xda\xda\xff\xff\xff\ +\xc1\xbd\xc1\xbb\xbb\xbb\xb8\xb9\xb9\xb9\xb9\xb9\xaa\xaa\xaa\xd1\ +\xd1\xd1\xcd\xcd\xcd\xcc\xca\xcc\xc0\xc0\xc0\xa9\xa9\xa9\xa6\xa7\ +\xa6\xa8\xa8\xa8\xce\xce\xce\xcc\xcc\xcc\xc9\xc9\xc9\xb6\xb9\xb9\ +\xac\xac\xac\xa7\xa7\xa7\xa5\xa6\xa5\xa4\xa4\xa4\xcc\xcb\xcc\xc8\ +\xc8\xc8\xbf\xbf\xbf\xa7\xa9\xa7\xa6\xa6\xa6\xa3\xa3\xa3\xa5\xa5\ +\xa5\xcd\xcc\xcd\xca\xca\xca\xa9\xac\xa9\xcf\xcf\xcf\xcb\xcb\xcb\ +\xaf\xaf\xaf\xa2\xa2\xa2\xdd\xdd\xdd\xdc\xdc\xdc\xd2\xd2\xd2\xcb\ +\xc9\xcb\xc2\xc2\xc2\xb8\xb8\xb8\x9c\x9c\x9c\x94\x94\x94\xde\xde\ +\xde\xdb\xdb\xdb\xd0\xd0\xd0\xc3\xc3\xc3\xbd\xbc\xbd\xad\xb3\xb3\ +\xa8\xa9\xa8\xa2\xa3\xa2\x91\x91\x91\x96\x96\x96\x95\x95\x95\x92\ +\x93\x92\x8f\x8f\x8f\x7f\x7f\x7f\xe0\xe0\xe0\xdf\xdf\xdf\xd8\xd8\ +\xd8\xbe\xbe\xbe\x99\x99\x99\x97\x97\x97\x92\x92\x92\x8e\x8e\x8e\ +\xe1\xe1\xe1\xa4\xa7\xa4\xa0\xa0\xa0\x9b\x9b\x9b\x90\x90\x90\x8d\ +\x8d\x8d\x98\x98\x98\x93\x93\x93\x8b\x8b\x8b\x8c\x8c\x8c\xe7\xe7\ +\xe7\xe2\xe2\xe2\xe9\xe9\xe9\xea\xea\xea\xe8\xe8\xe8\x84\x84\x84\ +\x86\x86\x86\x82\x82\x82\x85\x85\x85\xeb\xeb\xeb\xec\xec\xec}\ +}}|||{{{\xed\xed\xed\xef\xef\xefyy\ +yxxx\xee\xee\xee\xf0\xf0\xf0zzz+++\ +(((333ssslllooo-\ +--&&&'''hhhiiiqq\ +q888)))ccecccfef\ +gggnnn,,,***UUU2\ +22444VVV[[[bbbdd\ +dfff555AAADDDQQQ\ +RRRZZZeeejjj1117\ +77===CCCFFFNNNPP\ +PSSSWWW???OOOeef\ +IIIMMM@@@TTT666B\ +BB>>>HHH\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\xff\ +\x00=\x09\x1cH\xb0\xe0\x81\x03\x05\x13*\x5cH\xf0 \ +\xc3\x87\x04'L0\x88\x90\xa0\x02\x05\x10\x0bJ\xa4X\ +\xf0\xa2\x90\x8c\x03%N\x1c\xe8p\xe0E$ #n\ +\x14XR\xa0\x02!\x18S\x0a\x14I\xb2\xa2'\x8f\x19\ +Q$\xa4\xe9\xa9\xe4\xc9\x84\x1f\x09\xa2\x18\xaa3\xe4\xca\ +\x035\x5c\x22\x89)PH\x11!V\x84\x12\x1dj4\ +\xe1K\x82V\xa0fM8\xb5hF!`\xb72\x9c\ +\xfa\xd5\x8aY\x90d\x1f\x9a\x0d*\xb3\xad\xdb\xb7\x03\xc5\ +\xc8E1W\x0c]\x90a\xf2\xea\xb5\x92W\xae\xdf\xbf\ +b\xf0\xea\x1d\x1c\x06\xae\xe1\xc3\x10\xd9(f\xf30\x90\ +\xe3@\x19\x17+\xce\xf8\x182\xc3\xc5g\x18{:s\ +&\xa1\x9e\x81\x95-\x13\xc4\xacY g\x81\x84\x06\xea\ +\xf9\x0c\xba\xf2\xe8\xcc\xa57\x9f\xf6D(\xb5\xa7\xd5\xac\ +\x05\xba\x1e]\x90\xf3\xec\xda\xa8W'\x14\xcd\xd0\xf7@\ +\xe0\xc1s\xa74>\xf0\x8c\xed\xdb\xb8\xdb2G\xfd\x1c\ +\xbar\x88\xd3\xa9\x17\x8c\xbe\xbc3A\xe7\xdb\xaf\xbf\x08\ +E\x8e8!\xf9\x8c\x01\x01\x00!\xf9\x04\x01\x00\x00\xa8\ +\x00,\x00\x00\x00\x00 \x00 \x00\x87\x00\x00\x00\xd4\xd4\ +\xd4\xcf\xcf\xcf\xcd\xcd\xcd\xc6\xc6\xc6\xda\xda\xda\xce\xce\xce\ +\xcc\xcc\xcc\xff\xff\xff\xdd\xdd\xdd\xdc\xdc\xdc\x7f\x7f\x7f\xbe\ +\xbe\xbe\xbb\xbb\xbb\xba\xba\xba\xdf\xdf\xdf\xde\xde\xde\xdb\xdb\ +\xdb\xd2\xd2\xd2\xc0\xc0\xc0\xb7\xb9\xb9\xe1\xe1\xe1\xe9\xe9\xe9\ +\xbc\xbc\xbc\xb8\xb9\xb9\xb8\xb8\xb8\xe0\xe0\xe0\xb9\xb9\xb9\xe6\ +\xe6\xe6\xd8\xd8\xd8\xc1\xc1\xc1\xb7\xb7\xb7\xbf\xbf\xbf\xec\xec\ +\xec\xee\xee\xee\xe2\xe2\xe2\xca\xca\xca\xb1\xb1\xb1\xa8\xa9\xa9\ +\xa7\xa9\xa9\xeb\xeb\xeb\xe8\xe8\xe8\xe4\xe4\xe4\xd3\xd3\xd3\xcb\ +\xcb\xcc\xc2\xc2\xc2\x99\x99\x99\xb6\xb6\xb6\xab\xab\xab\xa7\xa7\ +\xa7\xa6\xa7\xa7\xa4\xa5\xa5\xed\xed\xed\xea\xea\xea\xe3\xe3\xe3\ +\xb7\xb8\xb8\xad\xad\xad\xa5\xa5\xa5\xa3\xa4\xa3\xa3\xa5\xa3\xe5\ +\xe5\xe5\xaf\xaf\xaf\xa9\xaa\xaa\xa5\xa6\xa6\xa4\xa4\xa4\xa3\xa4\ +\xa4\xf0\xf0\xf0\xe7\xe7\xe7\xa6\xa6\xa6\xa3\xa3\xa3\xa4\xa6\xa4\ +\xf2\xf2\xf2\xa8\xa8\xa8\xa2\xa2\xa2333...0\ +00---UUU\x9a\x9a\x9a\x99\x9c\x9c\x98\x9b\ +\x98\x97\x97\x97''',,,\x93\x96\x96\x92\x92\x92\ +\x91\x91\x91((()))\x90\x90\x90\x8d\x8d\x8d\x8e\ +\x8e\x8e\x8f\x8f\x8f777///+++\x8b\x8b\ +\x8b\x8c\x8c\x8c;;;366\x81\x81\x81\x83\x87\x87\ +888444222{{{|||}\ +~~\x87\x87\x87:::6664557:\ +:wwwyyy|}}\x80\x82\x82233\ +???CCCEEEsssjjjl\ +llxxxzzz{||~~~\x82\x82\ +\x82344555BBB@@@SSS\ +VVVaaadddhhhooo\x5c\ +\x5c\x5c===TTTAAADDDFF\ +FQQQTUUYYYbbbfff\ +qqq<<|\ +\xcc\xf8)0h\x02\x11+Q\xcd\xb4\xf8Q`R\x17\ +>\x5c$\x14\x0a5\xe5\xc3\xab>\x920\xe4:\xf4k\ +R\xa6\x0f\xb9B\xbc\x1a\xb3\xad\xdb\xb7\x05\xa7\xc8\x9d;\ +\x17e\x95*V\x5c\xb4\xd1\xab\x97\xae_\x94m\x02\x07\ +\xd6\xbb\x17\xae\xe1\xc3\x10\x95(V\xf2P\x8e\xe36\x19\ +\x17+\xce\xe8\xb82C\xc9\x8c\x05BJhi\xa0c\ +3\x8e\x13b&\xa8dsAK\x9d=W\x86>>\xa3\xa3\xa3\xa6\xa6\xa6\xa4\xa4\xa4===\ +;;;\xa5\xa5\xa5777444555\xa7\ +\xa7\xa7\xa6\xa7\xa6\xa7\xa8\xa7\xa8\xa8\xa822233\ +3\xb0\xb0\xb0\xa9\xa9\xa9\xac\xac\xac\xab\xab\xab\xab\xae\xab\ +\xb7\xb7\xb7\xba\xba\xba888\xb8\xb8\xb8\xb9\xb9\xb9\xbc\ +\xbc\xbc)))***///\xbf\xbf\xbf\xbb\xbb\ +\xbb\xbe\xc1\xc1+++'''(((\xba\xbb\xbb\ +\xbd\xbe\xbe\xc1\xc1\xc1\xcc\xcc\xcc\xcd\xcd\xcd\xec\xec\xec\xed\ +\xed\xed\xef\xef\xef111\xbb\xbc\xbc\xbd\xbd\xbd\xca\xca\ +\xca\xcb\xca\xcb\xce\xce\xce\xd2\xd2\xd2\xde\xde\xde\xe2\xe2\xe2\ +\xe9\xe9\xe9\xeb\xeb\xeb,,,\xb6\xb6\xb6\xd4\xd4\xd4\xc9\ +\xc9\xc9\xcf\xcf\xcf\xd3\xd3\xd3\xdc\xdc\xdc\xdf\xdf\xdf\xe6\xe6\ +\xe6\xea\xea\xea\xfe\xfe\xfe000\xbb\xbd\xbd\xbe\xbe\xbe\ +\xd0\xd0\xd0\xe7\xe7\xe7\xee\xee\xee---\xda\xda\xda\xdd\ +\xdd\xdd\xe8\xe8\xe8\xf4\xf4\xf4\xcb\xcb\xcb\xe1\xe1\xe1\xe4\xe4\ +\xe4\xe0\xe0\xe0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x08\xff\x009\x09\x1cH\xb0\xe0\x80\x01\x05\x13*\x5cH\ +\xf0 \xc3\x87\x04\x13$0\x88\x90\xa0\x08\x11\x10\x09z\ +\x90H\xb1\xe0\xc5 \x19\x07J\x9c8\xd0\xe1\xc0\x8b\x18\ +C\x0a\xe4\xc0\xb1dE\x81\x1fU\x8al\xc9\xc9$'\ +\x94\x199$\x1c)\xd0$\xce\x82 \x09r\x18\xaas\ +&I\x9f\x17\x09\x06Y\x1aT \x12\x0eO\x8b\x0a\xa4\ +y\xb2)'\xa6Z\xac\xae|\xfat\xa0\xd4\x93\x03\xb1\ +j-8\xb4+D\xa6K3F=\x1b$\xab\xcc\xb7\ +p\xe3\x16\xfc\xf1\x03\x89\xdd\xbauUj\xd9\xcb\x97/\ +\x12\xba\x80\x01\xeb\xed\xdbW\xae\xe1\xc3\x10\xd5(\x86\xd8\ +\xa6\xcd\xde\x90\x8a\x173n\xdc\xe6\xa1\xe2A\x92\x17J\ +\x1aH\xb9q\xc2\xc8\x999\xc9\x91\x93P\xd2f\x81\x9d\ +=\x0f\x04Mp4iN\x95*\x09\xacd\x9a`\xe7\ +\x82j\x12\xba\x9e-\x9b\x93\xe9\xd3\xb63\xee\xe6=\x90\ +6p\x95\xc3\x89\x0b\xfc}\x5c\xb8\x9c\xde\xca\x97\xd7F\ +>\xba`l\xeb\xd3CV'x\x9d`v\xc3\xdd\x11\ +'\x04\x0c\x9f1 \x00;\ \x00\x00\x01\x8a\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -3675,6 +4439,11 @@ \x00s\ \x00h\x00o\x00t\x00g\x00u\x00n\x00_\x00a\x00u\x00t\x00h\x00e\x00n\x00t\x00i\x00c\ \x00a\x00t\x00i\x00o\x00n\ +\x00\x12\ +\x0d\xb9\x836\ +\x00s\ +\x00p\x00i\x00n\x00n\x00i\x00n\x00g\x00_\x00w\x00h\x00e\x00e\x00l\x00.\x00g\x00i\ +\x00f\ \x00\x0e\ \x04\xac<\xa7\ \x00d\ @@ -3697,22 +4466,24 @@ " qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x07\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x08\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00.\x00\x02\x00\x00\x00\x01\x00\x00\x00\x06\ +\x00\x00\x00.\x00\x02\x00\x00\x00\x01\x00\x00\x00\x07\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00L\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04\ +\x00\x00\x00L\x00\x02\x00\x00\x00\x02\x00\x00\x00\x05\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00~\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x8f05\xd1\xf4\ -\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x01\x00\x00\x01\x8e\ -\x00\x00\x01\x8f05\xd1\xf8\ -\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x01\x00\x00\xb99\ -\x00\x00\x01\x8f05\xd1\xf3\ -\x00\x00\x01\x18\x00\x00\x00\x00\x00\x01\x00\x00\xdb\x22\ -\x00\x00\x01\x8f05\xd1\xf5\ +\x00\x00\x01\x97\x8f$\xb0j\ +\x00\x00\x00\xa8\x00\x00\x00\x00\x00\x01\x00\x00/\x9d\ +\x00\x00\x01\x97\x8f$\xb0N\ +\x00\x00\x00\xca\x00\x00\x00\x00\x00\x01\x00\x001+\ +\x00\x00\x01\x96\xcb\xb7\xc7v\ +\x00\x00\x01\x0a\x00\x00\x00\x00\x00\x01\x00\x00\xe8\xd6\ +\x00\x00\x01\x90,\xa0\x05b\ +\x00\x00\x01B\x00\x00\x00\x00\x00\x01\x00\x01\x0a\xbf\ +\x00\x00\x01\x90,\xa0\x05b\ " def qInitResources(): From a2ee46ba25efd8838374fe649ccbb8f9ab4b6d1b Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Fri, 20 Jun 2025 12:34:21 -0700 Subject: [PATCH 15/20] Spinner step3: Add a label in the UI to display the spinner icon --- .../authentication/resources/login_dialog.ui | 25 +++++++++++++++++++ python/tank/authentication/ui/login_dialog.py | 14 +++++++++++ 2 files changed, 39 insertions(+) diff --git a/python/tank/authentication/resources/login_dialog.ui b/python/tank/authentication/resources/login_dialog.ui index 0e1e94dd20..bb9af41d0f 100644 --- a/python/tank/authentication/resources/login_dialog.ui +++ b/python/tank/authentication/resources/login_dialog.ui @@ -251,6 +251,31 @@ QPushButton.main:pressed + + + + + + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + + + + Retrieving the site information... + + + Qt::AlignHCenter|Qt::AlignVCenter + + + + diff --git a/python/tank/authentication/ui/login_dialog.py b/python/tank/authentication/ui/login_dialog.py index 7bb3bc2da7..21353bc6f1 100644 --- a/python/tank/authentication/ui/login_dialog.py +++ b/python/tank/authentication/ui/login_dialog.py @@ -207,6 +207,18 @@ def setupUi(self, LoginDialog): self.verticalLayout_7.addWidget(self.password) + self.refresh_site_info_spinner = QLabel(self.credentials) + self.refresh_site_info_spinner.setObjectName(u"refresh_site_info_spinner") + self.refresh_site_info_spinner.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + + self.verticalLayout_7.addWidget(self.refresh_site_info_spinner) + + self.refresh_site_info_label = QLabel(self.credentials) + self.refresh_site_info_label.setObjectName(u"refresh_site_info_label") + self.refresh_site_info_label.setAlignment(Qt.AlignHCenter|Qt.AlignVCenter) + + self.verticalLayout_7.addWidget(self.refresh_site_info_label) + self.message = QLabel(self.credentials) self.message.setObjectName(u"message") sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) @@ -489,6 +501,8 @@ def retranslateUi(self, LoginDialog): self.password.setAccessibleName(QCoreApplication.translate("LoginDialog", u"password", None)) #endif // QT_CONFIG(accessibility) self.password.setPlaceholderText(QCoreApplication.translate("LoginDialog", u"password", None)) + self.refresh_site_info_spinner.setText("") + self.refresh_site_info_label.setText(QCoreApplication.translate("LoginDialog", u"Retrieving the site information...", None)) self.message.setText(QCoreApplication.translate("LoginDialog", u"Please enter your credentials.", None)) self.button_options.setText(QCoreApplication.translate("LoginDialog", u"See other options", None)) self.forgot_password_link.setText(QCoreApplication.translate("LoginDialog", u"

Forgot your password?

", None)) From e41f258438bcb176687f07fa162a1713ecb84d71 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Wed, 18 Jun 2025 16:29:43 -0700 Subject: [PATCH 16/20] WIP add spinner and raise questions --- python/tank/authentication/login_dialog.py | 47 ++++++++++++++++++---- python/tank/authentication/site_info.py | 15 +++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index c29f2bb65a..cb153fee46 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -337,6 +337,19 @@ def __init__( self.confirm_box.setStyleSheet(self.styleSheet()) + # Init UI Spinner + self.ui.spinner_movie = QtGui.QMovie(":/spinning_wheel.gif") + if self.ui.spinner_movie.loopCount() != -1: + self.ui.spinner_movie.finished.connect(self.ui.spinner_movie.start) + # TODO get a better GIF that loop forever + + self.ui.refresh_site_info_spinner.setMovie(self.ui.spinner_movie) + + # TODO: Init which widget are display by default + # IF not host at all in the recent box: nothing but the site widget + # IF host exists: spinner mode + + def __del__(self): """ Destructor. @@ -399,6 +412,16 @@ def _update_ui_according_to_site_support(self): TODO the name and description of this method are not accurate!!!!! """ + self.ui.login.setVisible(False) + self.ui.password.setVisible(False) + self.ui.message.setVisible(False) + self.ui.forgot_password_link.setVisible(False) + self.ui.button_options.setVisible(False) + self.ui.sign_in.setVisible(False) # maybe setEnable instead? + + self.ui.refresh_site_info_spinner.setVisible(True) + self.ui.refresh_site_info_label.setVisible(True) + logger.debug("_update_ui_according_to_site_support") self._query_task.url_to_test = self._get_current_site() self._query_task.start() @@ -558,6 +581,11 @@ def _toggle_web(self, method_selected=None): self.method_selected = method_selected + self.ui.refresh_site_info_spinner.setVisible(False) + self.ui.refresh_site_info_label.setVisible(False) + self.ui.message.setVisible(True) + self.ui.sign_in.setVisible(True) + # if we are switching from one mode (using the web) to another (not using # the web), or vice-versa, we need to update the GUI. # In web-based authentication, the web form is in charge of obtaining @@ -701,13 +729,18 @@ def result(self): return self._sso_saml2.get_session_data() - # We want to wait until we know what is supported by the site, to avoid - # flickering GUI. - if not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): - logger.warning( - "Timed out awaiting requesting information: %s" - % self._get_current_site() - ) + # # We want to wait until we know what is supported by the site, to avoid + # # flickering GUI. + # if not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): + # logger.warning( + # "Timed out awaiting requesting information: %s" + # % self._get_current_site() + # ) + + # Configure the GUI according to what we know: site and user preferences + # TODO -> self._toggle_web.... + + self.ui.spinner_movie.start() res = self.exec_() if res != QtGui.QDialog.Accepted: diff --git a/python/tank/authentication/site_info.py b/python/tank/authentication/site_info.py index 28a4c5b80e..d732731e17 100644 --- a/python/tank/authentication/site_info.py +++ b/python/tank/authentication/site_info.py @@ -40,6 +40,13 @@ def _get_site_infos(url, http_proxy=None): :returns: A dictionary with the site infos. """ + time.sleep(3) + return { + "user_authentication_method": "oxygen", + "unified_login_flow_enabled": True, + "authentication_app_session_launcher_enabled": True, + } + # logger.info("Sleep for 10s") # time.sleep(30) # logger.info("done sleeping") @@ -115,6 +122,14 @@ def reload(self, url, http_proxy=None): logger.debug("Unable to connect with %s, got exception '%s'", url, exc) return + + # TODO emit a signal with the infos dict instead of waiting for the comsumer to retrieve it. + # Because the thread might already run with different URL at that point.... + + + # ALSO, the following logs should only run if needed + # ALSO, why don't we consume the cache here ?! + self._url = url self._infos = infos From 9c7d2240d3b9bb43fb5b883cb9fd6093e21ae50f Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Tue, 24 Jun 2025 07:57:21 -0700 Subject: [PATCH 17/20] More changes and tests --- .../authentication/console_authentication.py | 3 +- python/tank/authentication/login_dialog.py | 148 +++++++++++------- python/tank/authentication/site_info.py | 49 +++--- 3 files changed, 117 insertions(+), 83 deletions(-) diff --git a/python/tank/authentication/console_authentication.py b/python/tank/authentication/console_authentication.py index 679a6aa46a..98e36de674 100644 --- a/python/tank/authentication/console_authentication.py +++ b/python/tank/authentication/console_authentication.py @@ -75,8 +75,7 @@ def authenticate(self, hostname, login, http_proxy): hostname = sanitize_url(hostname) - site_i = site_info.SiteInfo() - site_i.reload(hostname, http_proxy) + site_i = site_info.SiteInfo(hostname, http_proxy) if not site_i.app_session_launcher_enabled: # Will raise an exception if using a username/password pair is diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index cb153fee46..155a8bc308 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -91,28 +91,41 @@ class QuerySiteAndUpdateUITask(QtCore.QThread): to avoid blocking the main GUI thread. """ - def __init__(self, parent, site_info_instance, http_proxy=None): - """ - Constructor. - """ - QtCore.QThread.__init__(self, parent) - self._site_info = site_info_instance - self._http_proxy = http_proxy + _url_next = None - @property - def url_to_test(self): - """String R/W property.""" - return self._url_to_test + succeed = QtCore.Signal(str, site_info.SiteInfo) + failed = QtCore.Signal(str, Exception) + + def __init__(self, parent=None, http_proxy=None): + super().__init__(parent=parent) + self._http_proxy = http_proxy - @url_to_test.setter - def url_to_test(self, value): - self._url_to_test = value + def next_url(self, url: str) -> None: + self._url_next = url + # TODO Maybe need thread acquire release there... def run(self): """ Runs the thread. """ - self._site_info.reload(self._url_to_test, self._http_proxy) + + logger.info("QuerySiteAndUpdateUITask::run start") + + while self._url_next: + url = self._url_next + self._url_next = None + # TODO Maybe need thread acquire release there... + + + logger.info(f"QuerySiteAndUpdateUITask::loop {url}") + + try: + info = site_info.SiteInfo(url, http_proxy=self._http_proxy) + except Exception as exc: + self.failed.emit(url, exc) + else: + self.succeed.emit(url, info) + class LoginDialog(QtGui.QDialog): """ @@ -305,12 +318,9 @@ def __init__( self.ui.site.activated.connect(self._on_site_changed) self.ui.site.lineEdit().editingFinished.connect(self._on_site_changed) - self.site_info = site_info.SiteInfo() - - self._query_task = QuerySiteAndUpdateUITask(self, self.site_info, http_proxy) - self._query_task.finished.connect(self._toggle_web) - - self._on_site_changed() # trigger the first site info request + self._query_task = QuerySiteAndUpdateUITask(self, http_proxy=http_proxy) + self._query_task.succeed.connect(self._on_site_info_response) + self._query_task.failed.connect(self._on_site_info_response) # TODO # Initialize exit confirm message box self.confirm_box = QtGui.QMessageBox( @@ -335,6 +345,8 @@ def __init__( "will result in canceling your request." ) + self.site_info = None + self.confirm_box.setStyleSheet(self.styleSheet()) # Init UI Spinner @@ -349,6 +361,8 @@ def __init__( # IF not host at all in the recent box: nothing but the site widget # IF host exists: spinner mode + self._on_site_changed() # trigger the first site info request + def __del__(self): """ @@ -405,27 +419,6 @@ def _get_current_user(self): """ return sgutils.ensure_str(self.ui.login.currentText().strip()) - def _update_ui_according_to_site_support(self): - """ - Updates the GUI according to the site's information, hiding or showing - the username/password fields. - TODO the name and description of this method are not accurate!!!!! - """ - - self.ui.login.setVisible(False) - self.ui.password.setVisible(False) - self.ui.message.setVisible(False) - self.ui.forgot_password_link.setVisible(False) - self.ui.button_options.setVisible(False) - self.ui.sign_in.setVisible(False) # maybe setEnable instead? - - self.ui.refresh_site_info_spinner.setVisible(True) - self.ui.refresh_site_info_label.setVisible(True) - - logger.debug("_update_ui_according_to_site_support") - self._query_task.url_to_test = self._get_current_site() - self._query_task.start() - def _site_url_changing(self, text): """ Starts a timer to wait until the user stops entering the URL . @@ -447,9 +440,26 @@ def _on_site_changed(self): self.host_selected = host_selected + # TODO: get site_info cached. If valid results, no need for spinner icon and thread + self.ui.login.clear() ## TODO do we really want to do that here? self._populate_user_dropdown(self._get_current_site()) # same TOTO - self._update_ui_according_to_site_support() + + self.ui.login.setVisible(False) + self.ui.password.setVisible(False) + self.ui.message.setVisible(False) + self.ui.forgot_password_link.setVisible(False) + self.ui.button_options.setVisible(False) + self.ui.sign_in.setVisible(False) # maybe setEnable instead? + + self.ui.refresh_site_info_spinner.setVisible(True) + self.ui.refresh_site_info_label.setVisible(True) + + logger.debug("Call thread") + + self._query_task.next_url(self._get_current_site()) + self._query_task.start() + def _populate_user_dropdown(self, site): """ @@ -494,18 +504,23 @@ def _link_activated(self, site=None): self.ui.message, "Can't open '%s'." % forgot_password ) - def _toggle_web(self, method_selected=None): + def _toggle_web( ## TODO rename this method + self, + auth_method: int|None=None, + site_url: str|None=None, + site_info: site_info.SiteInfo|None=None, + ): """ Sets up the dialog GUI according to the use of web login or not. """ - site = self._query_task.url_to_test - if self._get_current_site() != site: - logger.debug("_toggle_web - site-info thread finished too late, we already selected another host") - # the thread finished too late, We already selected another host - return - # Careful, this method is called from a lot different use cases.......!!!!!! + logger.info(f"on _toggle_web auth_method: {auth_method}") + site = site_url or self._get_current_site() + if site_info: + self.site_info = site_info + + method_selected = auth_method # self.method_selected_user = None ## WHY ????? # We only update the GUI if there was a change between to mode we @@ -572,7 +587,7 @@ def _toggle_web(self, method_selected=None): else: method_selected = auth_constants.METHOD_BASIC - if method_selected == self.method_selected: + if auth_method and method_selected == self.method_selected: # We don't want to go further if the UI is already configured for # this site and this mode. # This prevents erasing any error message when various events would @@ -645,13 +660,38 @@ def _toggle_web(self, method_selected=None): ) def _menu_activated_action_asl(self): - self._toggle_web(method_selected=auth_constants.METHOD_ASL) + self._toggle_web(auth_method=auth_constants.METHOD_ASL) def _menu_activated_action_web_legacy(self): - self._toggle_web(method_selected=auth_constants.METHOD_WEB_LOGIN) + self._toggle_web(auth_method=auth_constants.METHOD_WEB_LOGIN) def _menu_activated_action_login_creds(self): - self._toggle_web(method_selected=auth_constants.METHOD_BASIC) + self._toggle_web(auth_method=auth_constants.METHOD_BASIC) + + + def _on_site_info_response( + self, + site_url: str, + response: site_info.SiteInfo|Exception, + ): + logger.info(f"_on_site_info_response - site_url: {site_url}; response: {response}") + + if self._get_current_site() != site_url: + logger.debug("_toggle_web - site-info thread finished too late, we already selected another host") + # the thread finished too late, We already selected another host + return + # Careful, this method is called from a lot different use cases.......!!!!!! + + if isinstance(response, Exception): + self.ui.refresh_site_info_spinner.setVisible(False) + self.ui.refresh_site_info_label.setVisible(False) + self.ui.message.setVisible(True) + self._set_error_message( + self.ui.message, f"Unable to communicate ith FPTR site.
{response}", + ) + else: + print("self.site_info._infos:", response._infos) + self._toggle_web(site_url=site_url, site_info=response) def _current_page_changed(self, index): """ diff --git a/python/tank/authentication/site_info.py b/python/tank/authentication/site_info.py index d732731e17..a9cd501876 100644 --- a/python/tank/authentication/site_info.py +++ b/python/tank/authentication/site_info.py @@ -41,6 +41,10 @@ def _get_site_infos(url, http_proxy=None): """ time.sleep(3) + + if url.endswith("2"): + raise NotImplementedError("URL not valid") + return { "user_authentication_method": "oxygen", "unified_login_flow_enabled": True, @@ -86,11 +90,7 @@ def _get_site_infos(url, http_proxy=None): class SiteInfo(object): - def __init__(self): - self._url = None - self._infos = {} - - def reload(self, url, http_proxy=None): + def __init__(self, url: str, http_proxy:str | None = None): """ Load the site information into the instance. @@ -103,6 +103,10 @@ def reload(self, url, http_proxy=None): :param url: Url of the site to query. :param http_proxy: HTTP proxy to use, if any. """ + logger.info("site_info start") + + self._url = url + # Check for valid URL url_items = utils.urlparse.urlparse(url) if ( @@ -111,16 +115,14 @@ def reload(self, url, http_proxy=None): or url_items.scheme not in ["http", "https"] ): logger.debug("Invalid Flow Production Tracking URL %s" % url) - return + raise Exception("Invalid URL") - infos = {} - try: - infos = _get_site_infos(url, http_proxy) - # pylint: disable=broad-except - except Exception as exc: - # Silently ignore exceptions - logger.debug("Unable to connect with %s, got exception '%s'", url, exc) - return + self._infos = _get_site_infos(url, http_proxy) + # # pylint: disable=broad-except + # except Exception as exc: + # # Silently ignore exceptions + # logger.debug("Unable to connect with %s, got exception '%s'", url, exc) + # return # TODO emit a signal with the infos dict instead of waiting for the comsumer to retrieve it. @@ -130,26 +132,19 @@ def reload(self, url, http_proxy=None): # ALSO, the following logs should only run if needed # ALSO, why don't we consume the cache here ?! - self._url = url - self._infos = infos - - logger.debug("Site info for {url}".format(url=self._url)) + logger.debug(f"Site info for {self._url}") logger.debug( - " user_authentication_method: {value}".format( - value=self.user_authentication_method, - ) + f" user_authentication_method: {self.user_authentication_method}" ) logger.debug( - " unified_login_flow_enabled: {value}".format( - value=self.unified_login_flow_enabled, - ) + f" unified_login_flow_enabled: {self.unified_login_flow_enabled}" ) logger.debug( - " authentication_app_session_launcher_enabled: {value}".format( - value=self.app_session_launcher_enabled, - ) + f" authentication_app_session_launcher_enabled: {self.app_session_launcher_enabled}" ) + logger.info("site_info end") + @property def user_authentication_method(self): """ From 2db4e701ffd74101b4e2fd922424bf967836da85 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Tue, 24 Jun 2025 08:52:59 -0700 Subject: [PATCH 18/20] eeeee --- .../authentication/console_authentication.py | 2 +- python/tank/authentication/login_dialog.py | 87 ++++---- python/tank/authentication/site_info.py | 202 +++++++++--------- 3 files changed, 147 insertions(+), 144 deletions(-) diff --git a/python/tank/authentication/console_authentication.py b/python/tank/authentication/console_authentication.py index 98e36de674..db292f1cf1 100644 --- a/python/tank/authentication/console_authentication.py +++ b/python/tank/authentication/console_authentication.py @@ -75,7 +75,7 @@ def authenticate(self, hostname, login, http_proxy): hostname = sanitize_url(hostname) - site_i = site_info.SiteInfo(hostname, http_proxy) + site_i = site_info.get(hostname, http_proxy=http_proxy) if not site_i.app_session_launcher_enabled: # Will raise an exception if using a username/password pair is diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index 155a8bc308..cda3782147 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -40,7 +40,7 @@ qt_version_tuple, ) from . import app_session_launcher -from . import site_info +from . import site_info as sg_site_info from .sso_saml2 import ( SsoSaml2IncompletePySide2, SsoSaml2Toolkit, @@ -93,7 +93,7 @@ class QuerySiteAndUpdateUITask(QtCore.QThread): _url_next = None - succeed = QtCore.Signal(str, site_info.SiteInfo) + succeed = QtCore.Signal(str, sg_site_info.SiteInfo) failed = QtCore.Signal(str, Exception) def __init__(self, parent=None, http_proxy=None): @@ -116,11 +116,10 @@ def run(self): self._url_next = None # TODO Maybe need thread acquire release there... - logger.info(f"QuerySiteAndUpdateUITask::loop {url}") try: - info = site_info.SiteInfo(url, http_proxy=self._http_proxy) + info = sg_site_info.SiteInfo(url, http_proxy=self._http_proxy) except Exception as exc: self.failed.emit(url, exc) else: @@ -213,7 +212,8 @@ def __init__( self.ui.login.set_style_sheet(completer_style) self.ui.login.set_placeholder_text("login") - self._populate_user_dropdown(recent_hosts[0] if recent_hosts else None) + if recent_hosts: + self._populate_user_dropdown(recent_hosts[0]) # Timer to update the GUI according to the URL. # This is to make the UX smoother, as we do not check after each character @@ -320,7 +320,7 @@ def __init__( self._query_task = QuerySiteAndUpdateUITask(self, http_proxy=http_proxy) self._query_task.succeed.connect(self._on_site_info_response) - self._query_task.failed.connect(self._on_site_info_response) # TODO + self._query_task.failed.connect(self._on_site_info_failure) # Initialize exit confirm message box self.confirm_box = QtGui.QMessageBox( @@ -440,10 +440,12 @@ def _on_site_changed(self): self.host_selected = host_selected - # TODO: get site_info cached. If valid results, no need for spinner icon and thread + self._populate_user_dropdown(self._get_current_site()) - self.ui.login.clear() ## TODO do we really want to do that here? - self._populate_user_dropdown(self._get_current_site()) # same TOTO + site_info = sg_site_info.get(host_selected, cache_only=True) + if site_info: + self._update_login_page_ui(site_info=site_info) + return self.ui.login.setVisible(False) self.ui.password.setVisible(False) @@ -461,17 +463,17 @@ def _on_site_changed(self): self._query_task.start() - def _populate_user_dropdown(self, site): + def _populate_user_dropdown(self, site: str) -> None: """ Populate the combo box of users based on a given site. :param str site: Site to populate the user list for. """ - if site: - users = session_cache.get_recent_users(site) - self.ui.login.set_recent_items(users) - else: - users = [] + + self.ui.login.clear() + + users = session_cache.get_recent_users(site) + self.ui.login.set_recent_items(users) if users: # The first user in the list is the most recent, so pick it. @@ -504,22 +506,25 @@ def _link_activated(self, site=None): self.ui.message, "Can't open '%s'." % forgot_password ) - def _toggle_web( ## TODO rename this method + def _update_login_page_ui( self, auth_method: int|None=None, - site_url: str|None=None, - site_info: site_info.SiteInfo|None=None, + site_info: sg_site_info.SiteInfo|None=None, ): """ Sets up the dialog GUI according to the use of web login or not. """ - logger.info(f"on _toggle_web auth_method: {auth_method}") + logger.info(f"on _update_login_page_ui auth_method: {auth_method}") - site = site_url or self._get_current_site() + site = self._get_current_site() if site_info: + site = site_info.url self.site_info = site_info + + # what if url is empty or site_info is empty??? + method_selected = auth_method # self.method_selected_user = None ## WHY ????? @@ -660,38 +665,44 @@ def _toggle_web( ## TODO rename this method ) def _menu_activated_action_asl(self): - self._toggle_web(auth_method=auth_constants.METHOD_ASL) + self._update_login_page_ui(auth_method=auth_constants.METHOD_ASL) def _menu_activated_action_web_legacy(self): - self._toggle_web(auth_method=auth_constants.METHOD_WEB_LOGIN) + self._update_login_page_ui(auth_method=auth_constants.METHOD_WEB_LOGIN) def _menu_activated_action_login_creds(self): - self._toggle_web(auth_method=auth_constants.METHOD_BASIC) + self._update_login_page_ui(auth_method=auth_constants.METHOD_BASIC) def _on_site_info_response( self, - site_url: str, - response: site_info.SiteInfo|Exception, + site_info: sg_site_info.SiteInfo, ): - logger.info(f"_on_site_info_response - site_url: {site_url}; response: {response}") + logger.info(f"_on_site_info_response - site_url: {site_info.url}") + + if self._get_current_site() != site_info.url: + logger.debug("_update_login_page_ui - site-info thread finished too late, we already selected another host") + # the thread finished too late, We already selected another host + return + # Careful, this method is called from a lot different use cases.......!!!!!! + + self._update_login_page_ui(site_info=site_info) + + def _on_site_info_failure(self, site_url: str, exc: Exception): + logger.info(f"_on_site_info_failure - site_url: {site_url}; response: {exc}") if self._get_current_site() != site_url: - logger.debug("_toggle_web - site-info thread finished too late, we already selected another host") + logger.debug("_update_login_page_ui - site-info thread finished too late, we already selected another host") # the thread finished too late, We already selected another host return # Careful, this method is called from a lot different use cases.......!!!!!! - if isinstance(response, Exception): - self.ui.refresh_site_info_spinner.setVisible(False) - self.ui.refresh_site_info_label.setVisible(False) - self.ui.message.setVisible(True) - self._set_error_message( - self.ui.message, f"Unable to communicate ith FPTR site.
{response}", - ) - else: - print("self.site_info._infos:", response._infos) - self._toggle_web(site_url=site_url, site_info=response) + self.ui.refresh_site_info_spinner.setVisible(False) + self.ui.refresh_site_info_label.setVisible(False) + self.ui.message.setVisible(True) + self._set_error_message( + self.ui.message, f"Unable to communicate ith FPTR site.
{exc}", + ) def _current_page_changed(self, index): """ @@ -778,7 +789,7 @@ def result(self): # ) # Configure the GUI according to what we know: site and user preferences - # TODO -> self._toggle_web.... + # TODO -> self._update_login_page_ui.... self.ui.spinner_movie.start() diff --git a/python/tank/authentication/site_info.py b/python/tank/authentication/site_info.py index a9cd501876..8f3f394cd0 100644 --- a/python/tank/authentication/site_info.py +++ b/python/tank/authentication/site_info.py @@ -19,118 +19,18 @@ logger = LogManager.get_logger(__name__) - -# Cache the servers infos for 30 seconds. -INFOS_CACHE_TIMEOUT = 30 -# This is a global state variable. It is used to cache information about the Shotgun servers we -# are interacting with. This is purely to avoid making multiple calls to the servers which would -# yield back the same information. (That info is relatively constant on a given server) -# Should this variable be cleared when doing a Python Core swap, it is not an issue. -# The side effect would be an additional call to the Shotgun site. -INFOS_CACHE = {} - - -def _get_site_infos(url, http_proxy=None): - """ - Get and cache the desired site infos. - - :param url: Url of the site to query. - :param http_proxy: HTTP proxy to use, if any. - - :returns: A dictionary with the site infos. - """ - - time.sleep(3) - - if url.endswith("2"): - raise NotImplementedError("URL not valid") - - return { - "user_authentication_method": "oxygen", - "unified_login_flow_enabled": True, - "authentication_app_session_launcher_enabled": True, - } - - # logger.info("Sleep for 10s") - # time.sleep(30) - # logger.info("done sleeping") - - # Checks if the information is in the cache, is missing or out of date. - if url not in INFOS_CACHE or ( - (time.time() - INFOS_CACHE[url][0]) > INFOS_CACHE_TIMEOUT - ): - # Temporary shotgun instance, used only for the purpose of checking - # the site infos. - # - # The constructor of Shotgun requires either a username/login or - # key/scriptname pair or a session_token. The token is only used in - # calls which need to be authenticated. The 'info' call does not - # require authentication. - http_proxy = utils.sanitize_http_proxy(http_proxy).netloc - if http_proxy: - logger.debug("Using HTTP proxy to connect to the PTR server: %s", http_proxy) - - logger.info("Infos for site '%s' not in cache or expired", url) - sg = shotgun_api3.Shotgun( - url, session_token="dummy", connect=False, http_proxy=http_proxy - ) - # Remove delay between attempts at getting the site info. Since - # this is called in situations where blocking during multiple - # attempts can make UIs less responsive, we'll avoid sleeping. - # This change was introduced after delayed retries were added in - # python-api v3.0.41 - sg.config.rpc_attempt_interval = 0 - infos = sg.info() - - INFOS_CACHE[url] = (time.time(), infos) - else: - logger.info("Infos for site '%s' found in cache", url) - - return INFOS_CACHE[url][1] - - -class SiteInfo(object): - def __init__(self, url: str, http_proxy:str | None = None): +class SiteInfo: + def __init__(self, url: str, info: dict): """ - Load the site information into the instance. - - We want this method to fail as quickly as possible if there are any - issues. Failure is not considered critical, thus known exceptions are - silently ignored. At the moment this method used by the GUI show/hide - some of the input fields and by the console authentication to select the - appropriate authentication method. + TODO :param url: Url of the site to query. - :param http_proxy: HTTP proxy to use, if any. + :param info: site infor given by the API. """ logger.info("site_info start") self._url = url - - # Check for valid URL - url_items = utils.urlparse.urlparse(url) - if ( - not url_items.netloc - or url_items.netloc in "https" - or url_items.scheme not in ["http", "https"] - ): - logger.debug("Invalid Flow Production Tracking URL %s" % url) - raise Exception("Invalid URL") - - self._infos = _get_site_infos(url, http_proxy) - # # pylint: disable=broad-except - # except Exception as exc: - # # Silently ignore exceptions - # logger.debug("Unable to connect with %s, got exception '%s'", url, exc) - # return - - - # TODO emit a signal with the infos dict instead of waiting for the comsumer to retrieve it. - # Because the thread might already run with different URL at that point.... - - - # ALSO, the following logs should only run if needed - # ALSO, why don't we consume the cache here ?! + self._infos = info logger.debug(f"Site info for {self._url}") logger.debug( @@ -145,6 +45,10 @@ def __init__(self, url: str, http_proxy:str | None = None): logger.info("site_info end") + @property + def url(self): + return self._url + @property def user_authentication_method(self): """ @@ -202,3 +106,91 @@ def app_session_launcher_enabled(self): """ return self._infos.get("authentication_app_session_launcher_enabled", False) + +# Cache the servers infos for 30 seconds. +INFOS_CACHE_TIMEOUT = 30 +# This is a global state variable. It is used to cache information about the Shotgun servers we +# are interacting with. This is purely to avoid making multiple calls to the servers which would +# yield back the same information. (That info is relatively constant on a given server) +# Should this variable be cleared when doing a Python Core swap, it is not an issue. +# The side effect would be an additional call to the Shotgun site. +INFOS_CACHE = {} + + +def get(url: str, http_proxy: str|None=None, cache_only=False) -> SiteInfo|None: + """ + Get and cache the desired site infos. + + :param url: Url of the site to query. + :param http_proxy: HTTP proxy to use, if any. + # TODO cache_only and better desc + + :returns: A SiteInfo instance or None + """ + + # Checks if the information is in the cache, is missing or out of date. + if url not in INFOS_CACHE or ( + (time.time() - INFOS_CACHE[url][0]) > INFOS_CACHE_TIMEOUT + ): + if cache_only: + return + + si = _retrieve_site_info(url, http_proxy=http_proxy) + INFOS_CACHE[url] = (time.time(), si) + else: + logger.info(f'Infos for site "{url}" found in cache') + + return INFOS_CACHE[url][1] + +def _retrieve_site_info(url, http_proxy=None): + """ + Load the site information into the instance. + + We want this method to fail as quickly as possible if there are any + issues. Failure is not considered critical, thus known exceptions are + silently ignored. At the moment this method used by the GUI show/hide + some of the input fields and by the console authentication to select the + appropriate authentication method. + + """ + + # Check for valid URL + url_items = utils.urlparse.urlparse(url) + if ( + not url_items.netloc + or url_items.netloc in "https" + or url_items.scheme not in ["http", "https"] + ): + logger.debug("Invalid Flow Production Tracking URL %s" % url) + raise Exception("Invalid URL") + + # Temporary shotgun instance, used only for the purpose of checking + # the site infos. + # + # The constructor of Shotgun requires either a username/login or + # key/scriptname pair or a session_token. The token is only used in + # calls which need to be authenticated. The 'info' call does not + # require authentication. + http_proxy = utils.sanitize_http_proxy(http_proxy).netloc + if http_proxy: + logger.debug("Using HTTP proxy to connect to the PTR server: %s", http_proxy) + + logger.info("Infos for site '%s' not in cache or expired", url) + sg = shotgun_api3.Shotgun( + url, session_token="dummy", connect=False, http_proxy=http_proxy + ) + # Remove delay between attempts at getting the site info. Since + # this is called in situations where blocking during multiple + # attempts can make UIs less responsive, we'll avoid sleeping. + # This change was introduced after delayed retries were added in + # python-api v3.0.41 + sg.config.rpc_attempt_interval = 0 + + try: + infos = sg.info() + except Exception as exc: + # Silently ignore exceptions + logger.debug("Unable to connect with %s, got exception '%s'", url, exc) + return + + return SiteInfo(url, infos) From 96fdf028b6ec6a9e3616427c2c2259a6f62d3e32 Mon Sep 17 00:00:00 2001 From: Julien Langlois Date: Tue, 24 Jun 2025 10:54:33 -0700 Subject: [PATCH 19/20] wewewerrewerw --- python/tank/authentication/login_dialog.py | 96 +++++++++++----------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index cda3782147..303e8c9d49 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -439,12 +439,12 @@ def _on_site_changed(self): return self.host_selected = host_selected + self.site_info = sg_site_info.get(host_selected, cache_only=True) - self._populate_user_dropdown(self._get_current_site()) + self._populate_user_dropdown(host_selected) - site_info = sg_site_info.get(host_selected, cache_only=True) - if site_info: - self._update_login_page_ui(site_info=site_info) + if self.site_info: + self._update_login_page_ui() return self.ui.login.setVisible(False) @@ -462,6 +462,35 @@ def _on_site_changed(self): self._query_task.next_url(self._get_current_site()) self._query_task.start() + def _on_site_info_response(self, site_info: sg_site_info.SiteInfo): + logger.info(f"_on_site_info_response - site_url: {site_info.url}") + + if self._get_current_site() != site_info.url: + logger.debug("_update_login_page_ui - site-info thread finished too late, we already selected another host") + return + + self.site_info = site_info + + self.ui.refresh_site_info_spinner.setVisible(False) + self.ui.refresh_site_info_label.setVisible(False) + self.ui.message.setVisible(True) + self.ui.sign_in.setVisible(True) + + self._update_login_page_ui() + + def _on_site_info_failure(self, site_url: str, exc: Exception): + logger.info(f"_on_site_info_failure - site_url: {site_url}; response: {exc}") + + if self._get_current_site() != site_url: + logger.debug("_update_login_page_ui - site-info thread finished too late, we already selected another host") + return + + self.ui.refresh_site_info_spinner.setVisible(False) + self.ui.refresh_site_info_label.setVisible(False) + self.ui.message.setVisible(True) + self._set_error_message( + self.ui.message, f"Unable to communicate ith FPTR site.
{exc}", + ) def _populate_user_dropdown(self, site: str) -> None: """ @@ -506,11 +535,7 @@ def _link_activated(self, site=None): self.ui.message, "Can't open '%s'." % forgot_password ) - def _update_login_page_ui( - self, - auth_method: int|None=None, - site_info: sg_site_info.SiteInfo|None=None, - ): + def _update_login_page_ui(self, auth_method: int|None=None): """ Sets up the dialog GUI according to the use of web login or not. """ @@ -518,9 +543,15 @@ def _update_login_page_ui( logger.info(f"on _update_login_page_ui auth_method: {auth_method}") site = self._get_current_site() - if site_info: - site = site_info.url - self.site_info = site_info + if self.site_info: + site = self.site_info.url + + if not self.site_info: + # thread is working at getting the info but we try to provide a best + # guess here. + prev_selected_method = session_cache.get_preferred_method(site) + else: + prev_selected_method = None # what if url is empty or site_info is empty??? @@ -536,7 +567,7 @@ def _update_login_page_ui( if can_use_web: # With a SSO site, we have no choice but to use the web to login. - can_use_web = self.site_info.sso_enabled + can_use_web = self.site_info.sso_enabled # TODO FIXME: in this situation, the legacy login/password method should not be available # The user may decide to force the use of the old dialog: # - due to graphical issues with Qt and its WebEngine @@ -556,6 +587,8 @@ def _update_login_page_ui( if method_selected: # Selecting requested mode (credentials, qt_web_login or app_session_launcher) self.method_selected_user = method_selected + # WHY ???????? TODO FIXME + # should be pass instead elif os.environ.get("SGTK_FORCE_STANDARD_LOGIN_DIALOG"): # Selecting legacy auth by default method_selected = auth_constants.METHOD_BASIC @@ -601,11 +634,6 @@ def _update_login_page_ui( self.method_selected = method_selected - self.ui.refresh_site_info_spinner.setVisible(False) - self.ui.refresh_site_info_label.setVisible(False) - self.ui.message.setVisible(True) - self.ui.sign_in.setVisible(True) - # if we are switching from one mode (using the web) to another (not using # the web), or vice-versa, we need to update the GUI. # In web-based authentication, the web form is in charge of obtaining @@ -647,6 +675,7 @@ def _update_login_page_ui( self.ui.forgot_password_link.setVisible( method_selected == auth_constants.METHOD_BASIC + and self.site_info and self.site_info.user_authentication_method in ["default", "ldap"] ) @@ -673,37 +702,6 @@ def _menu_activated_action_web_legacy(self): def _menu_activated_action_login_creds(self): self._update_login_page_ui(auth_method=auth_constants.METHOD_BASIC) - - def _on_site_info_response( - self, - site_info: sg_site_info.SiteInfo, - ): - logger.info(f"_on_site_info_response - site_url: {site_info.url}") - - if self._get_current_site() != site_info.url: - logger.debug("_update_login_page_ui - site-info thread finished too late, we already selected another host") - # the thread finished too late, We already selected another host - return - # Careful, this method is called from a lot different use cases.......!!!!!! - - self._update_login_page_ui(site_info=site_info) - - def _on_site_info_failure(self, site_url: str, exc: Exception): - logger.info(f"_on_site_info_failure - site_url: {site_url}; response: {exc}") - - if self._get_current_site() != site_url: - logger.debug("_update_login_page_ui - site-info thread finished too late, we already selected another host") - # the thread finished too late, We already selected another host - return - # Careful, this method is called from a lot different use cases.......!!!!!! - - self.ui.refresh_site_info_spinner.setVisible(False) - self.ui.refresh_site_info_label.setVisible(False) - self.ui.message.setVisible(True) - self._set_error_message( - self.ui.message, f"Unable to communicate ith FPTR site.
{exc}", - ) - def _current_page_changed(self, index): """ Resets text error message on the destination page. From 6f2efcc1933f2d3ef1308838d14a6cc32803faf2 Mon Sep 17 00:00:00 2001 From: Eduardo Chauca Date: Fri, 31 Oct 2025 19:43:44 -0500 Subject: [PATCH 20/20] Loading its working after a correct domain in site field --- python/tank/authentication/login_dialog.py | 92 +++++++++++++-------- python/tank/authentication/site_info.py | 96 +++++++++++----------- 2 files changed, 105 insertions(+), 83 deletions(-) diff --git a/python/tank/authentication/login_dialog.py b/python/tank/authentication/login_dialog.py index 303e8c9d49..6e7cfac5fb 100644 --- a/python/tank/authentication/login_dialog.py +++ b/python/tank/authentication/login_dialog.py @@ -91,39 +91,29 @@ class QuerySiteAndUpdateUITask(QtCore.QThread): to avoid blocking the main GUI thread. """ - _url_next = None - succeed = QtCore.Signal(str, sg_site_info.SiteInfo) failed = QtCore.Signal(str, Exception) - def __init__(self, parent=None, http_proxy=None): + def __init__(self, url, http_proxy=None, parent=None): super().__init__(parent=parent) + self._url = url self._http_proxy = http_proxy - - def next_url(self, url: str) -> None: - self._url_next = url - # TODO Maybe need thread acquire release there... - + def run(self): """ Runs the thread. """ - logger.info("QuerySiteAndUpdateUITask::run start") - - while self._url_next: - url = self._url_next - self._url_next = None - # TODO Maybe need thread acquire release there... - - logger.info(f"QuerySiteAndUpdateUITask::loop {url}") + logger.info(f"QuerySiteAndUpdateUITask::run start for {self._url}") - try: - info = sg_site_info.SiteInfo(url, http_proxy=self._http_proxy) - except Exception as exc: - self.failed.emit(url, exc) - else: - self.succeed.emit(url, info) + try: + info = sg_site_info.get(self._url, http_proxy=self._http_proxy) + except Exception as exc: + logger.error(f"Failed to get site info for {self._url}: {exc}") + self.failed.emit(self._url, exc) + else: + logger.info(f"Successfully got site info for {self._url}") + self.succeed.emit(self._url, info) class LoginDialog(QtGui.QDialog): @@ -318,9 +308,8 @@ def __init__( self.ui.site.activated.connect(self._on_site_changed) self.ui.site.lineEdit().editingFinished.connect(self._on_site_changed) - self._query_task = QuerySiteAndUpdateUITask(self, http_proxy=http_proxy) - self._query_task.succeed.connect(self._on_site_info_response) - self._query_task.failed.connect(self._on_site_info_failure) + self._query_task = None # Will be created when needed + self._http_proxy = http_proxy # Initialize exit confirm message box self.confirm_box = QtGui.QMessageBox( @@ -369,7 +358,8 @@ def __del__(self): Destructor. """ # We want to clean up any running qthread. - self._query_task.wait() + if self._query_task: + self._query_task.wait() def _confirm_exit(self): return self.confirm_box.exec_() == QtGui.QMessageBox.StandardButton.Yes @@ -381,6 +371,10 @@ def closeEvent(self, event): event.ignore() return + # Stop the query thread if running + if self._query_task and self._query_task.isRunning(): + self._query_task.wait() + if self._asl_task: self._asl_task.finished.disconnect(self._asl_task_finished) self._asl_task.stop_when_possible() @@ -395,6 +389,10 @@ def keyPressEvent(self, event): event.ignore() return + # Stop the query thread if running + if self._query_task and self._query_task.isRunning(): + self._query_task.wait() + if self._asl_task: self._asl_task.finished.disconnect(self._asl_task_finished) self._asl_task.stop_when_possible() @@ -457,39 +455,63 @@ def _on_site_changed(self): self.ui.refresh_site_info_spinner.setVisible(True) self.ui.refresh_site_info_label.setVisible(True) - logger.debug("Call thread") + logger.debug("Creating new thread for site info request") - self._query_task.next_url(self._get_current_site()) + # Stop any existing thread first + if self._query_task and self._query_task.isRunning(): + self._query_task.wait() + + # Create a new thread for this request + self._query_task = QuerySiteAndUpdateUITask( + url=self._get_current_site(), + http_proxy=self._http_proxy, + parent=self + ) + + # Connect signals for this new thread + self._query_task.succeed.connect(self._on_site_info_response) + self._query_task.failed.connect(self._on_site_info_failure) + + # Make sure signals are delivered to main thread + self._query_task.finished.connect(lambda: logger.debug("Thread finished")) + self._query_task.start() + + logger.debug(f"Thread started for URL: {self._get_current_site()}") - def _on_site_info_response(self, site_info: sg_site_info.SiteInfo): - logger.info(f"_on_site_info_response - site_url: {site_info.url}") + def _on_site_info_response(self, site_url: str, site_info): + logger.info(f"_on_site_info_response - site_url: {site_url}") + logger.debug(f"_on_site_info_response - current site: {self._get_current_site()}") - if self._get_current_site() != site_info.url: - logger.debug("_update_login_page_ui - site-info thread finished too late, we already selected another host") + if self._get_current_site() != site_url: + logger.debug("_on_site_info_response - site-info thread finished too late, we already selected another host") return self.site_info = site_info + logger.debug("_on_site_info_response - hiding spinner and showing UI elements") self.ui.refresh_site_info_spinner.setVisible(False) self.ui.refresh_site_info_label.setVisible(False) self.ui.message.setVisible(True) self.ui.sign_in.setVisible(True) + logger.debug("_on_site_info_response - calling _update_login_page_ui") self._update_login_page_ui() def _on_site_info_failure(self, site_url: str, exc: Exception): logger.info(f"_on_site_info_failure - site_url: {site_url}; response: {exc}") + logger.debug(f"_on_site_info_failure - current site: {self._get_current_site()}") if self._get_current_site() != site_url: - logger.debug("_update_login_page_ui - site-info thread finished too late, we already selected another host") + logger.debug("_on_site_info_failure - site-info thread finished too late, we already selected another host") return + logger.debug("_on_site_info_failure - hiding spinner and showing error") self.ui.refresh_site_info_spinner.setVisible(False) self.ui.refresh_site_info_label.setVisible(False) self.ui.message.setVisible(True) self._set_error_message( - self.ui.message, f"Unable to communicate ith FPTR site.
{exc}", + self.ui.message, f"Unable to communicate with FPTR site.
{exc}", ) def _populate_user_dropdown(self, site: str) -> None: @@ -847,7 +869,7 @@ def _ok_pressed(self): # Wait for any ongoing Site Configuration check thread. QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) try: - if not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): + if self._query_task and not self._query_task.wait(THREAD_WAIT_TIMEOUT_MS): logger.warning( "Timed out awaiting configuration information on the site: %s" % self._get_current_site() diff --git a/python/tank/authentication/site_info.py b/python/tank/authentication/site_info.py index 8f3f394cd0..b96d35f149 100644 --- a/python/tank/authentication/site_info.py +++ b/python/tank/authentication/site_info.py @@ -143,54 +143,54 @@ def get(url: str, http_proxy: str|None=None, cache_only=False) -> SiteInfo|None: return INFOS_CACHE[url][1] def _retrieve_site_info(url, http_proxy=None): - """ - Load the site information into the instance. - - We want this method to fail as quickly as possible if there are any - issues. Failure is not considered critical, thus known exceptions are - silently ignored. At the moment this method used by the GUI show/hide - some of the input fields and by the console authentication to select the - appropriate authentication method. + """ + Load the site information into the instance. - """ + We want this method to fail as quickly as possible if there are any + issues. Failure is not considered critical, thus known exceptions are + silently ignored. At the moment this method used by the GUI show/hide + some of the input fields and by the console authentication to select the + appropriate authentication method. - # Check for valid URL - url_items = utils.urlparse.urlparse(url) - if ( - not url_items.netloc - or url_items.netloc in "https" - or url_items.scheme not in ["http", "https"] - ): - logger.debug("Invalid Flow Production Tracking URL %s" % url) - raise Exception("Invalid URL") - - # Temporary shotgun instance, used only for the purpose of checking - # the site infos. - # - # The constructor of Shotgun requires either a username/login or - # key/scriptname pair or a session_token. The token is only used in - # calls which need to be authenticated. The 'info' call does not - # require authentication. - http_proxy = utils.sanitize_http_proxy(http_proxy).netloc - if http_proxy: - logger.debug("Using HTTP proxy to connect to the PTR server: %s", http_proxy) - - logger.info("Infos for site '%s' not in cache or expired", url) - sg = shotgun_api3.Shotgun( - url, session_token="dummy", connect=False, http_proxy=http_proxy - ) - # Remove delay between attempts at getting the site info. Since - # this is called in situations where blocking during multiple - # attempts can make UIs less responsive, we'll avoid sleeping. - # This change was introduced after delayed retries were added in - # python-api v3.0.41 - sg.config.rpc_attempt_interval = 0 - - try: - infos = sg.info() - except Exception as exc: - # Silently ignore exceptions - logger.debug("Unable to connect with %s, got exception '%s'", url, exc) - return + """ - return SiteInfo(url, infos) + # Check for valid URL + url_items = utils.urlparse.urlparse(url) + if ( + not url_items.netloc + or url_items.netloc in "https" + or url_items.scheme not in ["http", "https"] + ): + logger.debug("Invalid Flow Production Tracking URL %s" % url) + raise Exception("Invalid URL") + + # Temporary shotgun instance, used only for the purpose of checking + # the site infos. + # + # The constructor of Shotgun requires either a username/login or + # key/scriptname pair or a session_token. The token is only used in + # calls which need to be authenticated. The 'info' call does not + # require authentication. + http_proxy = utils.sanitize_http_proxy(http_proxy).netloc + if http_proxy: + logger.debug("Using HTTP proxy to connect to the PTR server: %s", http_proxy) + + logger.info("Infos for site '%s' not in cache or expired", url) + sg = shotgun_api3.Shotgun( + url, session_token="dummy", connect=False, http_proxy=http_proxy + ) + # Remove delay between attempts at getting the site info. Since + # this is called in situations where blocking during multiple + # attempts can make UIs less responsive, we'll avoid sleeping. + # This change was introduced after delayed retries were added in + # python-api v3.0.41 + sg.config.rpc_attempt_interval = 0 + + try: + infos = sg.info() + except Exception as exc: + # Silently ignore exceptions + logger.debug("Unable to connect with %s, got exception '%s'", url, exc) + return + + return SiteInfo(url, infos)