Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use SG API key for creating session and remove login functionality #58

Merged
merged 13 commits into from Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 17 additions & 23 deletions 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": [
Expand All @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where do you normally define this environment variable, and what should happen if it's not set empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on my case I'm actually just using $USER as all our artists machines have that defined so we don't need any extra user input to define their login for SG but that's why I left it open to discuss. We could potentially expose an option on the settings where admins can choose any existing env var to use as SG login and/or optionally expose the login menu so each user can set it themselves on the registry

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)
63 changes: 7 additions & 56 deletions 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.
Expand All @@ -57,26 +20,14 @@ 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()

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))

93 changes: 80 additions & 13 deletions client/ayon_shotgrid/plugins/publish/integrate_shotgrid_publish.py
@@ -1,4 +1,5 @@
import os
import re
import platform

import pyblish.api
Expand All @@ -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")

Expand All @@ -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],
Expand All @@ -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"
)
Expand All @@ -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:
Expand Down Expand Up @@ -141,3 +168,43 @@ def process(self, instance):
)
instance.data["shotgridPublishedFile"] = sg_published_file

def _find_published_file_type(self, instance, filepath, representation):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function is a pretty simplistic way to set some types on the PublishedFiles of SG and it relies on setting these on your SG instance:
Screenshot 2023-12-01 at 1 38 55 PM

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is great too, how would you select what SH publish type (representation) you're publishing too?
Do I need to make sure I name the SG and AYON representations exactly the same?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

atm I'm not making use of the Ayon family or representation name, I'm purely basing it off the extension of the file type and you don't have to do anything extra, just that this function detects all the file types you want to track

"""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