diff --git a/client/ayon_shotgrid/addon.py b/client/ayon_shotgrid/addon.py index 0f1577cc..f5a35935 100644 --- a/client/ayon_shotgrid/addon.py +++ b/client/ayon_shotgrid/addon.py @@ -1,28 +1,35 @@ import os +import ayon_api + from openpype.modules import ( OpenPypeModule, - ITrayModule, IPluginPaths, ) SHOTGRID_MODULE_DIR = os.path.dirname(os.path.abspath(__file__)) -class ShotgridAddon(OpenPypeModule, ITrayModule, IPluginPaths): +class ShotgridAddon(OpenPypeModule, IPluginPaths): name = "shotgrid" enabled = True - tray_wrapper = None def initialize(self, modules_settings): module_settings = modules_settings.get(self.name, dict()) self._shotgrid_server_url = module_settings.get("shotgrid_server") - self._shotgrid_script_name = None - self._shotgrid_api_key = None + sg_secret = ayon_api.get_secret(module_settings["shotgrid_api_secret"]) + self._shotgrid_script_name = sg_secret.get("name") + self._shotgrid_api_key = sg_secret.get("value") def get_sg_url(self): return self._shotgrid_server_url if self._shotgrid_server_url else None + def get_sg_script_name(self): + return self._shotgrid_script_name if self._shotgrid_script_name else None + + def get_sg_api_key(self): + return self._shotgrid_api_key if self._shotgrid_api_key else None + def get_plugin_paths(self): return { "publish": [ @@ -33,26 +40,13 @@ def get_plugin_paths(self): def create_shotgrid_session(self): from .lib import credentials - sg_username, sg_password = credentials.get_local_login() - - if not sg_username or not sg_password: - return None + sg_username = os.getenv("AYON_SG_USERNAME") + proxy = os.environ.get("HTTPS_PROXY", "").lstrip("https://") return credentials.create_sg_session( self._shotgrid_server_url, sg_username, - sg_password + self._shotgrid_script_name, + self._shotgrid_api_key, + proxy, ) - - def tray_init(self): - from .tray.shotgrid_tray import ShotgridTrayWrapper - self.tray_wrapper = ShotgridTrayWrapper(self) - - def tray_start(self): - return self.tray_wrapper.set_username_label() - - def tray_exit(self, *args, **kwargs): - return self.tray_wrapper - - def tray_menu(self, tray_menu): - return self.tray_wrapper.tray_menu(tray_menu) diff --git a/client/ayon_shotgrid/lib/credentials.py b/client/ayon_shotgrid/lib/credentials.py index 38abe98d..a2e71e86 100644 --- a/client/ayon_shotgrid/lib/credentials.py +++ b/client/ayon_shotgrid/lib/credentials.py @@ -1,52 +1,15 @@ import shotgun_api3 -from shotgun_api3.shotgun import AuthenticationFault -from openpype.lib import OpenPypeSettingsRegistry - -def check_user_permissions(shotgrid_url, username, password): - """Check if the provided user can access the Shotgrid API. - - Args: - shotgrid_url (str): The Shotgun server URL. - username (str): The Shotgrid login username. - password (str): The Shotgrid login password. - - Returns: - tuple(bool, str): Whether the connection was succsefull or not, and a - string message with the result. - """ - - if not shotgrid_url or not username or not password: - return (False, "Missing a field.") - - try: - session = create_sg_session( - shotgrid_url, - username, - password - ) - session.close() - except AuthenticationFault as e: - return (False, str(e)) - - return (True, "Succesfully logged in.") - - -def clear_local_login(): - """Clear the Shotgrid Login entry from the local registry. """ - reg = OpenPypeSettingsRegistry() - reg.delete_item("shotgrid_login") - - -def create_sg_session(shotgrid_url, username, password): +def create_sg_session(shotgrid_url, username, script_name, api_key, proxy): """Attempt to create a Shotgun Session Args: shotgrid_url (str): The Shotgun server URL. + username (str): The Shotgrid username to use the Session as. script_name (str): The Shotgrid API script name. api_key (str): The Shotgrid API key. - username (str): The Shotgrid username to use the Session as. + proxy (str): The proxy address to use to connect to SG server. Returns: session (shotgun_api3.Shotgun): A Shotgrid API Session. @@ -57,8 +20,10 @@ def create_sg_session(shotgrid_url, username, password): session = shotgun_api3.Shotgun( base_url=shotgrid_url, - login=username, - password=password, + script_name=script_name, + http_proxy=proxy, + api_key=api_key, + sudo_as_login=username, ) session.preferences_read() @@ -66,17 +31,3 @@ def create_sg_session(shotgrid_url, username, password): return session -def get_local_login(): - """Get the Shotgrid Login entry from the local registry. """ - reg = OpenPypeSettingsRegistry() - try: - return reg.get_item("shotgrid_login") - except Exception: - return (None, None) - - -def save_local_login(username, password): - """Save the Shotgrid Login entry from the local registry. """ - reg = OpenPypeSettingsRegistry() - reg.set_item("shotgrid_login", (username, password)) - diff --git a/client/ayon_shotgrid/plugins/publish/integrate_shotgrid_publish.py b/client/ayon_shotgrid/plugins/publish/integrate_shotgrid_publish.py index 64880f0c..a7b3c958 100644 --- a/client/ayon_shotgrid/plugins/publish/integrate_shotgrid_publish.py +++ b/client/ayon_shotgrid/plugins/publish/integrate_shotgrid_publish.py @@ -1,4 +1,5 @@ import os +import re import platform import pyblish.api @@ -17,6 +18,12 @@ class IntegrateShotgridPublish(pyblish.api.InstancePlugin): label = "Shotgrid Published Files" def process(self, instance): + # Skip execution if instance is marked to be processed in the farm + if instance.data.get("farm"): + self.log.info( + "Instance is marked to be processed on farm. Skipping") + return + sg_session = instance.context.data.get("shotgridSession") sg_version = instance.data.get("shotgridVersion") @@ -25,18 +32,33 @@ def process(self, instance): for representation in instance.data.get("representations", []): + if "shotgridreview" not in representation.get("tags", []): + self.log.debug( + "No 'shotgridreview' tag on representation '%s', skipping.", + representation.get("name") + ) + continue + local_path = get_publish_repre_path( instance, representation, False ) - if representation.get("tags", []): - continue - sg_project = instance.data.get("shotgridProject") sg_entity = instance.data.get("shotgridEntity") sg_task = instance.data.get("shotgridTask") code = os.path.basename(local_path) + # Extract and remove version number from code so Publishedfile versions are + # grouped together. More info about this on: + # https://developer.shotgridsoftware.com/tk-core/_modules/tank/util/shotgun/publish_creation.html + version_number = 0 + match = re.search("_v(\d+)", code) + if match: + version_number = int(match.group(1)) + # Remove version from name + code = re.sub("_v\d+", "", code) + # Remove frames from name (i.e., filename.1001.exr -> filename.exr) + code = re.sub("\.\d+", "", code) query_filters = [ ["project", "is", sg_project], @@ -53,41 +75,41 @@ def process(self, instance): query_filters ) - sg_local_store = sg_session.find_one( + sg_local_storage = sg_session.find_one( "LocalStorage", filters=[], - fields=["mac_path","windows_path", "linux_path"] + fields=["mac_path", "windows_path", "linux_path"] ) - if not sg_local_store: + if not sg_local_storage: KnownPublishError( - "Unable to find a Local Store in Shotgrid." + "Unable to find a Local Storage in Shotgrid." "Enable them in Site Preferences > Local Management:" "https://help.autodesk.com/view/SGSUB/ENU/?guid=SG_Administrator_ar_data_management_ar_linking_local_files_html" ) - self.log.debug("Using the Local Store: {sg_local_store}") + self.log.debug("Using the Local Storage: {sg_local_storage}") try: if platform.system() == "Windows": _, file_partial_path = local_path.split( - sg_local_store["windows_path"] + sg_local_storage["windows_path"] ) file_partial_path = file_partial_path.replace("\\", "/") elif platform.system() == "Linux": _, file_partial_path = local_path.split( - sg_local_store["linux_path"] + sg_local_storage["linux_path"] ) elif platform.system() == "Darwin": _, file_partial_path = local_path.split( - sg_local_store["mac_path"] + sg_local_storage["mac_path"] ) file_partial_path = file_partial_path.lstrip("/") except ValueError: raise KnownPublishError( f"Filepath {local_path} doesn't match the " - f"Shotgrid Local Store {sg_local_store}" + f"Shotgrid Local Storage {sg_local_storage}" "Enable them in Site Preferences > Local Management:" "https://help.autodesk.com/view/SGSUB/ENU/?guid=SG_Administrator_ar_data_management_ar_linking_local_files_html" ) @@ -98,9 +120,14 @@ def process(self, instance): "entity": sg_entity, "version": sg_version, "path": { - "local_storage": sg_local_store, + "local_storage": sg_local_storage, "relative_path": file_partial_path }, + # Add file type and version number fields + "published_file_type": self._find_published_file_type( + instance, local_path, representation + ), + "version_number": version_number, } if sg_task: @@ -141,3 +168,43 @@ def process(self, instance): ) instance.data["shotgridPublishedFile"] = sg_published_file + def _find_published_file_type(self, instance, filepath, representation): + """Given a filepath infer what type of published file type it is.""" + + _, ext = os.path.splitext(filepath) + published_file_type = "Unknown" + + if ext in [".exr", ".jpg", ".jpeg", ".png", ".dpx", ".tif", ".tiff"]: + is_sequence = len(representation["files"]) > 1 + if is_sequence: + published_file_type = "Rendered Image" + else: + published_file_type = "Image" + elif ext in [".mov", ".mp4"]: + published_file_type = "Movie" + elif ext == ".abc": + published_file_type = "Alembic Cache" + elif ext in [".bgeo", ".sc", ".gz"]: + published_file_type = "Bgeo Geo" + elif ext in [".ma", ".mb"]: + published_file_type = "Maya Scene" + elif ext == ".nk": + published_file_type = "Nuke Script" + elif ext == ".hip": + published_file_type = "Houdini Scene" + elif ext in [".hda"]: + published_file_type = "HDA" + elif ext in [".fbx"]: + published_file_type = "FBX Geo" + + filters = [["code", "is", published_file_type]] + sg_session = instance.context.data.get("shotgridSession") + sg_published_file_type = sg_session.find_one( + "PublishedFileType", filters=filters + ) + if not sg_published_file_type: + # Create a published file type on the fly + sg_published_file_type = sg_session.create( + "PublishedFileType", {"code": published_file_type} + ) + return sg_published_file_type \ No newline at end of file diff --git a/client/ayon_shotgrid/plugins/publish/integrate_shotgrid_version.py b/client/ayon_shotgrid/plugins/publish/integrate_shotgrid_version.py index 39df1cb7..b6083dea 100644 --- a/client/ayon_shotgrid/plugins/publish/integrate_shotgrid_version.py +++ b/client/ayon_shotgrid/plugins/publish/integrate_shotgrid_version.py @@ -1,3 +1,5 @@ +import re + import pyblish.api from openpype.pipeline.publish import get_publish_repre_path @@ -8,40 +10,27 @@ class IntegrateShotgridVersion(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.497 label = "Shotgrid Version" + # Dictionary of SG fields we want to update that map to other fields in the + # Ayon entity + fields_to_add = { + "comment": (str, "description"), + "family": (str, "sg_version_type"), + } + def process(self, instance): - sg_session = instance.context.data["shotgridSession"] - - # TODO: Use path template solver to build version code from settings - anatomy = instance.data.get("anatomyData", {}) - version_name = "_".join( - [ - anatomy["project"]["code"], - anatomy["parent"], - anatomy["asset"], - ] - ) - - if instance.data["shotgridTask"]: - version_name = "{0}_{1}".format( - version_name, - instance.data["shotgridTask"]["content"] - ) - - version_name = "{0}_{1}".format( - version_name, - "v{:03}".format(int(anatomy["version"])) - ) - - sg_version = self._find_existing_version(version_name, instance) - if not sg_version: - sg_version = self._create_version(version_name, instance) - self.log.info("Create Shotgrid version: {}".format(sg_version)) - else: - self.log.info("Use existing Shotgrid version: {}".format(sg_version)) + # Skip execution if instance is marked to be processed in the farm + if instance.data.get("farm"): + self.log.info("Instance is marked to be processed on farm. Skipping") + return + + context = instance.context + # Dictionary that holds all the data we want to set/update on + # the corresponding SG version data_to_update = {} - intent = instance.context.data.get("intent") + + intent = context.data.get("intent") if intent: data_to_update["sg_status_list"] = intent["value"] @@ -55,25 +44,105 @@ def process(self, instance): ) if representation["ext"] in ["mov", "avi"]: - self.log.info( - "Upload review: {} for version shotgrid {}".format( - local_path, sg_version.get("id") - ) - ) - sg_session.upload( - "Version", - sg_version.get("id"), - local_path, - field_name="sg_uploaded_movie", - ) - data_to_update["sg_path_to_movie"] = local_path - + if ( + "slate" in instance.data["families"] + and "slate-frame" in representation["tags"] + ): + data_to_update["sg_movie_has_slate"] = True + elif representation["ext"] in ["jpg", "png", "exr", "tga"]: - path_to_frame = local_path.replace("0000", "#") + # Replace the frame number with '%04d' + path_to_frame = re.sub(r"\.\d+\.", ".%04d.", local_path) + data_to_update["sg_path_to_frames"] = path_to_frame + if "slate" in instance.data["families"]: + data_to_update["sg_frames_have_slate"] = True + + # If there's no data to set/update, skip creation of SG version + if not data_to_update: + self.log.info("No data to integrate to SG, skipping version creation.") + return + + sg_session = context.data["shotgridSession"] - self.log.info("Update Shotgrid version with {}".format(data_to_update)) + # TODO: Use path template solver to build version code from settings + anatomy = instance.data.get("anatomyData", {}) + version_name_tokens = [ + anatomy["asset"], + instance.data["subset"], + ] + + if instance.data["shotgridTask"]: + version_name_tokens.append( + instance.data["shotgridTask"]["content"] + ) + + version_name_tokens.append( + "v{:03}".format(int(anatomy["version"])) + ) + + version_name = "_".join(version_name_tokens) + + self.log.info("Integrating Shotgrid version with code: {}".format(version_name)) + + sg_version = self._find_existing_version(version_name, instance) + if not sg_version: + sg_version = self._create_version(version_name, instance) + self.log.info("Create Shotgrid version: {}".format(sg_version)) + else: + self.log.info("Use existing Shotgrid version: {}".format(sg_version)) + + # Upload movie to version + path_to_movie = data_to_update.get("sg_path_to_movie") + if path_to_movie: + self.log.info( + "Upload review: {} for version shotgrid {}".format( + path_to_movie, sg_version.get("id") + ) + ) + sg_session.upload( + "Version", + sg_version.get("id"), + path_to_movie, + field_name="sg_uploaded_movie", + ) + + # Update frame start/end on the version + frame_start = instance.data.get("frameStart", context.data.get("frameStart")) + handle_start = instance.data.get("handleStart", context.data.get("handleStart")) + if frame_start is not None and handle_start is not None: + frame_start = int(frame_start) + handle_start = int(handle_start) + data_to_update["sg_first_frame"] = frame_start - handle_start + + frame_end = instance.data.get("frameEnd", context.data.get("frameEnd")) + handle_end = instance.data.get("handleEnd", context.data.get("handleEnd")) + if frame_end is not None and handle_end is not None: + frame_end = int(frame_end) + handle_end = int(handle_end) + data_to_update["sg_last_frame"] = frame_end + handle_end + + # Add a few extra fields from OP to SG version + for op_field, sg_field in self.fields_to_add.items(): + field_value = instance.data.get(op_field) or context.data.get(op_field) + if field_value: + # Break sg_field tuple into whatever type of data it is and its name + type_, field_name = sg_field + + data_to_update[field_name] = type_(field_value) + + # Add version objectId to "sg_op_instance_id" so we can keep a link + # between both + version_entity = instance.data.get("versionEntity", {}).get("_id") + if not version_entity: + self.log.warning( + "Instance doesn't have a 'versionEntity' to extract the id." + ) + version_entity = "-" + data_to_update["sg_op_instance_id"] = str(version_entity) + + self.log.info("Updating Shotgrid version with {}".format(data_to_update)) sg_session.update("Version", sg_version["id"], data_to_update) instance.data["shotgridVersion"] = sg_version diff --git a/client/ayon_shotgrid/plugins/publish/validate_shotgrid_user.py b/client/ayon_shotgrid/plugins/publish/validate_shotgrid_user.py index f0b16a49..139eea12 100644 --- a/client/ayon_shotgrid/plugins/publish/validate_shotgrid_user.py +++ b/client/ayon_shotgrid/plugins/publish/validate_shotgrid_user.py @@ -20,28 +20,48 @@ def process(self, context): raise PublishValidationError("Missing Shotgrid Credentials") self.log.info("Login Shotgrid set in Ayon is {}".format(user_login)) - self.log.info("Current shotgun Project is {}".format(sg_project)) + self.log.info("Current Shotgrid Project is {}".format(sg_project)) sg_user = sg_session.find_one( "HumanUser", - [ - ["login", "is", user_login], - ["projects", "name_contains", project_name] - ], - ["projects"] + [["login", "is", user_login]], + ["projects", "permission_rule_set"], ) + sg_user_has_permission = False + + if sg_user: + sg_user_has_permission = sg_user["permission_rule_set"]["name"] == "Admin" + + # It's not an admin, but it might still have permissions + if not sg_user_has_permission: + for project in sg_user["projects"]: + if project["name"] == project_name: + sg_user_has_permission = True + break + + if not sg_user_has_permission: + raise PublishValidationError( + "Login {0} doesn't have access to the project {1} <{2}>".format( + user_login, project_name, sg_project + ) + ) + + self.log.info("Found User in Shotgrid: {}".format(sg_user)) + + admin = sg_user["permission_rule_set"]["name"] == "Admin" + self.log.info("Found User in Shotgrid: {}".format(sg_user)) - if not sg_user: + if not sg_user and not admin: raise PublishValidationError( - "Login {0} don't have access to the project {1} <{2}>".format( + "Login {0} doesn't have access to the project {1} <{2}>".format( user_login, project_name, sg_project ) ) self.log.info( - "Login {0} have access to the project {1} <{2}>".format( + "Login {0} has access to the project {1} <{2}>".format( user_login, project_name, sg_project ) ) diff --git a/client/ayon_shotgrid/tray/sg_login_dialog.py b/client/ayon_shotgrid/tray/sg_login_dialog.py deleted file mode 100644 index b8b5c9da..00000000 --- a/client/ayon_shotgrid/tray/sg_login_dialog.py +++ /dev/null @@ -1,115 +0,0 @@ -import os - -from ayon_shotgrid.lib import credentials - -import ayon_api -from openpype import style -from openpype import resources -from qtpy import QtCore, QtWidgets, QtGui - -class SgLoginDialog(QtWidgets.QDialog): - """A QDialog that allows the person to set a Shotgrid Username. - - It also allows them to test the username agains the API. - """ - - dialog_closed = QtCore.Signal() - - def __init__(self, module, parent=None): - super(SgLoginDialog, self).__init__(parent) - self.module = module - - self.setWindowTitle("Ayon - Shotgrid Login") - icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) - self.setWindowIcon(icon) - - self.setWindowFlags( - QtCore.Qt.WindowCloseButtonHint - | QtCore.Qt.WindowMinimizeButtonHint - ) - - self.setStyleSheet(style.load_stylesheet()) - self.setContentsMargins(2, 2, 2, 2) - - self.setup_ui() - - def closeEvent(self, event): - """Clear any message when closing the dialog.""" - self.sg_connection_message.setText("") - self.dialog_closed.emit() - super(SgLoginDialog, self).closeEvent(event) - - def setup_ui(self): - server_url = self.module.get_sg_url() - - if not server_url: - server_url = "No Shotgrid Server set in Ayon Settings." - - sg_server_url_label = QtWidgets.QLabel( - "Please provide the credentials to log in into the Shotgrid Server:\n{}".format(server_url) - ) - - dialog_layout = QtWidgets.QVBoxLayout() - dialog_layout.addWidget(sg_server_url_label) - - sg_username, sg_password = credentials.get_local_login() - - self.sg_username_input = QtWidgets.QLineEdit() - - if sg_username: - self.sg_username_input.setText(sg_username) - else: - self.sg_username_input.setPlaceholderText("jane.doe@mycompany.com") - - self.sg_password_input = QtWidgets.QLineEdit() - self.sg_password_input.setEchoMode(QtWidgets.QLineEdit.Password) - - if sg_password: - self.sg_password_input.setText(sg_password) - else: - self.sg_password_input.setPlaceholderText("c0mPre$Hi0n") - - dialog_layout.addWidget(QtWidgets.QLabel("Shotgrid Username:")) - dialog_layout.addWidget(self.sg_username_input) - - dialog_layout.addWidget(QtWidgets.QLabel("Shotgrid Password:")) - dialog_layout.addWidget(self.sg_password_input) - - self.sg_check_login_button = QtWidgets.QPushButton("Login into Shotgrid...") - self.sg_check_login_button.clicked.connect(self.check_sg_credentials) - self.sg_connection_message = QtWidgets.QLabel("") - - dialog_layout.addWidget(self.sg_check_login_button) - dialog_layout.addWidget(self.sg_connection_message) - - self.setLayout(dialog_layout) - - def set_local_login(self): - """Change Username label, save in local registry and set env var.""" - sg_username = self.sg_username_input.text() - sg_password = self.sg_password_input.text() - - if sg_username and sg_password: - credentials.save_local_login(sg_username, sg_password) - os.environ["AYON_SG_USERNAME"] = sg_username - else: - credentials.clear_local_login() - os.environ["AYON_SG_USERNAME"] = "" - - def check_sg_credentials(self): - """Check if the provided username can login via the API.""" - sg_username = self.sg_username_input.text() - sg_password = self.sg_password_input.text() - - login_result, login_message = credentials.check_user_permissions( - self.module.get_sg_url(), - sg_username, - sg_password, - ) - - self.set_local_login() - - if login_result: - self.close() - else: - self.sg_connection_message.setText(login_message) diff --git a/client/ayon_shotgrid/tray/shotgrid_tray.py b/client/ayon_shotgrid/tray/shotgrid_tray.py deleted file mode 100644 index 6a328355..00000000 --- a/client/ayon_shotgrid/tray/shotgrid_tray.py +++ /dev/null @@ -1,79 +0,0 @@ -import os - -from qtpy import QtWidgets - -from ayon_shotgrid.lib import credentials -from ayon_shotgrid.tray.sg_login_dialog import SgLoginDialog - - -class ShotgridTrayWrapper: - """ Shotgrid menu entry for the Ayon tray. - - Displays the Shotgrid URL specified in the Server Addon Settings and - allows the person to set a username to be used with the API. - - There's the option to check if said user has persmissions to connect to the - API. - """ - def __init__(self, module): - self.module = module - - server_url = self.module.get_sg_url() - - if not server_url: - server_url = "No Shotgrid Server set in Ayon Settings." - - self.sg_server_label = QtWidgets.QAction("Server: {0}".format( - server_url - ) - ) - self.sg_server_label.setDisabled(True) - self.sg_username_label = QtWidgets.QAction("") - self.sg_username_label.triggered.connect(self.show_sg_username_dialog) - - self.sg_username_dialog = SgLoginDialog(self.module) - self.sg_username_dialog.dialog_closed.connect(self.set_username_label) - - def show_sg_username_dialog(self): - """Display the Shotgrid Username dialog - - Used to set a Shotgird Username, that will then be used by any API call - and to check that the user can access the Shotgrid API. - """ - self.sg_username_dialog.show() - self.sg_username_dialog.activateWindow() - self.sg_username_dialog.raise_() - - def tray_menu(self, tray_menu): - """Add Shotgrid Submenu to Ayon tray. - - A non-actionable action displays the Shotgrid URL and the other - action allows the person to set and check their Shotgrid username. - - Args: - tray_menu (QtWidgets.QMenu): The Ayon Tray menu. - """ - shotgrid_tray_menu = QtWidgets.QMenu("Shotgrid", tray_menu) - shotgrid_tray_menu.addAction(self.sg_server_label) - shotgrid_tray_menu.addSeparator() - shotgrid_tray_menu.addAction(self.sg_username_label) - tray_menu.addMenu(shotgrid_tray_menu) - - def set_username_label(self): - """Set the Username Label based on local login setting. - - Depending on the login credentiasl we want to display one message or - another in the Shotgrid submenu action. - """ - sg_username, _ = credentials.get_local_login() - - if sg_username: - self.sg_username_label.setText( - "Username: {} (Click to change)".format(sg_username) - ) - os.environ["AYON_SG_USERNAME"] = sg_username - else: - self.sg_username_label.setText("Specify a Username...") - os.environ["AYON_SG_USERNAME"] = "" - self.show_sg_username_dialog() -