mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merged in feature/PYPE-159_ftrack_cleanup (pull request #53)
Feature/PYPE-159 ftrack cleanup Approved-by: Milan Kolar <milan@orbi.tools>
This commit is contained in:
commit
df4f5b8b41
43 changed files with 1404 additions and 2181 deletions
|
|
@ -1 +1 @@
|
|||
|
||||
from .lib import *
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import toml
|
||||
import time
|
||||
from ftrack_action_handler import AppAction
|
||||
from pype.ftrack import AppAction
|
||||
from avalon import lib
|
||||
from app.api import Logger
|
||||
from pype import lib as pypelib
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import getpass
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
|
||||
|
||||
class AssetDelete(BaseAction):
|
||||
|
|
@ -14,17 +13,17 @@ class AssetDelete(BaseAction):
|
|||
#: Action label.
|
||||
label = 'Asset Delete'
|
||||
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
||||
if (len(entities) != 1 or entities[0].entity_type
|
||||
not in ['Shot', 'Asset Build']):
|
||||
|
||||
if (
|
||||
len(entities) != 1 or
|
||||
entities[0].entity_type not in ['Shot', 'Asset Build']
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
|
||||
if not event['data'].get('values', {}):
|
||||
|
|
@ -38,10 +37,10 @@ class AssetDelete(BaseAction):
|
|||
label = asset['name']
|
||||
|
||||
items.append({
|
||||
'label':label,
|
||||
'name':label,
|
||||
'value':False,
|
||||
'type':'boolean'
|
||||
'label': label,
|
||||
'name': label,
|
||||
'value': False,
|
||||
'type': 'boolean'
|
||||
})
|
||||
|
||||
if len(items) < 1:
|
||||
|
|
@ -69,7 +68,7 @@ class AssetDelete(BaseAction):
|
|||
session.delete(asset)
|
||||
try:
|
||||
session.commit()
|
||||
except:
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import getpass
|
||||
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
|
||||
|
||||
class ClientReviewSort(BaseAction):
|
||||
|
|
@ -17,7 +15,6 @@ class ClientReviewSort(BaseAction):
|
|||
#: Action label.
|
||||
label = 'Sort Review'
|
||||
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
||||
|
|
@ -26,7 +23,6 @@ class ClientReviewSort(BaseAction):
|
|||
|
||||
return True
|
||||
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
|
||||
entity = entities[0]
|
||||
|
|
@ -40,7 +36,9 @@ class ClientReviewSort(BaseAction):
|
|||
|
||||
# Sort criteria
|
||||
obj_list = sorted(obj_list, key=lambda k: k['version'])
|
||||
obj_list = sorted(obj_list, key=lambda k: k['asset_version']['task']['name'])
|
||||
obj_list = sorted(
|
||||
obj_list, key=lambda k: k['asset_version']['task']['name']
|
||||
)
|
||||
obj_list = sorted(obj_list, key=lambda k: k['name'])
|
||||
|
||||
# Set 'sort order' to sorted list, so they are sorted in Ftrack also
|
||||
|
|
|
|||
|
|
@ -4,11 +4,10 @@
|
|||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import getpass
|
||||
import subprocess
|
||||
import os
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
|
||||
|
||||
class ComponentOpen(BaseAction):
|
||||
|
|
@ -19,8 +18,10 @@ class ComponentOpen(BaseAction):
|
|||
# Action label
|
||||
label = 'Open File'
|
||||
# Action icon
|
||||
icon = 'https://cdn4.iconfinder.com/data/icons/rcons-application/32/application_go_run-256.png',
|
||||
|
||||
icon = (
|
||||
'https://cdn4.iconfinder.com/data/icons/rcons-application/32/'
|
||||
'application_go_run-256.png'
|
||||
)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
|
@ -29,13 +30,13 @@ class ComponentOpen(BaseAction):
|
|||
|
||||
return True
|
||||
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
|
||||
entity = entities[0]
|
||||
|
||||
# Return error if component is on ftrack server
|
||||
if entity['component_locations'][0]['location']['name'] == 'ftrack.server':
|
||||
location_name = entity['component_locations'][0]['location']['name']
|
||||
if location_name == 'ftrack.server':
|
||||
return {
|
||||
'success': False,
|
||||
'message': "This component is stored on ftrack server!"
|
||||
|
|
@ -49,7 +50,7 @@ class ComponentOpen(BaseAction):
|
|||
fpath = os.sep.join(items)
|
||||
|
||||
if os.path.isdir(fpath):
|
||||
if 'win' in sys.platform: # windows
|
||||
if 'win' in sys.platform: # windows
|
||||
subprocess.Popen('explorer "%s"' % fpath)
|
||||
elif sys.platform == 'darwin': # macOS
|
||||
subprocess.Popen(['open', fpath])
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import argparse
|
|||
import json
|
||||
import ftrack_api
|
||||
import arrow
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction, get_ca_mongoid
|
||||
|
||||
"""
|
||||
This action creates/updates custom attributes.
|
||||
|
|
@ -117,15 +117,19 @@ class CustomAttributes(BaseAction):
|
|||
super().__init__(session)
|
||||
|
||||
templates = os.environ['PYPE_STUDIO_TEMPLATES']
|
||||
path_items = [templates, 'presets', 'ftrack', 'ftrack_custom_attributes.json']
|
||||
path_items = [
|
||||
templates, 'presets', 'ftrack', 'ftrack_custom_attributes.json'
|
||||
]
|
||||
self.filepath = os.path.sep.join(path_items)
|
||||
self.types = {}
|
||||
self.object_type_ids = {}
|
||||
self.groups = {}
|
||||
self.security_roles = {}
|
||||
self.required_keys = ['key', 'label', 'type']
|
||||
self.type_posibilities = ['text', 'boolean', 'date', 'enumerator',
|
||||
'dynamic enumerator', 'number']
|
||||
self.type_posibilities = [
|
||||
'text', 'boolean', 'date', 'enumerator',
|
||||
'dynamic enumerator', 'number'
|
||||
]
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
'''
|
||||
|
|
@ -166,13 +170,13 @@ class CustomAttributes(BaseAction):
|
|||
session.rollback()
|
||||
job['status'] = 'failed'
|
||||
session.commit()
|
||||
self.log.error("Creating custom attributes failed ({})".format(e))
|
||||
self.log.error('Creating custom attributes failed ({})'.format(e))
|
||||
|
||||
return True
|
||||
|
||||
def avalon_mongo_id_attributes(self, session):
|
||||
# Attribute Name and Label
|
||||
cust_attr_name = 'avalon_mongo_id'
|
||||
cust_attr_name = get_ca_mongoid()
|
||||
cust_attr_label = 'Avalon/Mongo Id'
|
||||
|
||||
# Types that don't need object_type_id
|
||||
|
|
@ -191,7 +195,7 @@ class CustomAttributes(BaseAction):
|
|||
for obj_type in all_obj_types:
|
||||
name = obj_type['name']
|
||||
if " " in name:
|
||||
name = name.replace(" ", "")
|
||||
name = name.replace(' ', '')
|
||||
|
||||
if obj_type['name'] not in self.object_type_ids:
|
||||
self.object_type_ids[name] = obj_type['id']
|
||||
|
|
@ -200,7 +204,7 @@ class CustomAttributes(BaseAction):
|
|||
filtered_types_id.add(obj_type['id'])
|
||||
|
||||
# Set security roles for attribute
|
||||
role_list = ["API", "Administrator"]
|
||||
role_list = ['API', 'Administrator']
|
||||
roles = self.get_security_role(role_list)
|
||||
# Set Text type of Attribute
|
||||
custom_attribute_type = self.get_type('text')
|
||||
|
|
@ -231,7 +235,10 @@ class CustomAttributes(BaseAction):
|
|||
with open(self.filepath) as data_file:
|
||||
json_dict = json.load(data_file)
|
||||
except Exception as e:
|
||||
msg = 'Loading "Custom attribute file" Failed. Please check log for more information'
|
||||
msg = (
|
||||
'Loading "Custom attribute file" Failed.'
|
||||
' Please check log for more information'
|
||||
)
|
||||
self.log.warning("{} - {}".format(msg, str(e)))
|
||||
self.show_message(event, msg)
|
||||
return
|
||||
|
|
@ -250,7 +257,9 @@ class CustomAttributes(BaseAction):
|
|||
self.process_attribute(data)
|
||||
|
||||
except CustAttrException as cae:
|
||||
msg = 'Custom attribute error "{}" - {}'.format(cust_attr_name, str(cae))
|
||||
msg = 'Custom attribute error "{}" - {}'.format(
|
||||
cust_attr_name, str(cae)
|
||||
)
|
||||
self.log.warning(msg)
|
||||
self.show_message(event, msg)
|
||||
|
||||
|
|
@ -260,16 +269,20 @@ class CustomAttributes(BaseAction):
|
|||
existing_atr = self.session.query('CustomAttributeConfiguration').all()
|
||||
matching = []
|
||||
for attr in existing_atr:
|
||||
if (attr['key'] != data['key'] or
|
||||
attr['type']['name'] != data['type']['name']):
|
||||
if (
|
||||
attr['key'] != data['key'] or
|
||||
attr['type']['name'] != data['type']['name']
|
||||
):
|
||||
continue
|
||||
|
||||
if 'is_hierarchical' in data:
|
||||
if data['is_hierarchical'] == attr['is_hierarchical']:
|
||||
matching.append(attr)
|
||||
elif 'object_type_id' in data:
|
||||
if (attr['entity_type'] == data['entity_type'] and
|
||||
attr['object_type_id'] == data['object_type_id']):
|
||||
if (
|
||||
attr['entity_type'] == data['entity_type'] and
|
||||
attr['object_type_id'] == data['object_type_id']
|
||||
):
|
||||
matching.append(attr)
|
||||
else:
|
||||
if attr['entity_type'] == data['entity_type']:
|
||||
|
|
@ -278,25 +291,40 @@ class CustomAttributes(BaseAction):
|
|||
if len(matching) == 0:
|
||||
self.session.create('CustomAttributeConfiguration', data)
|
||||
self.session.commit()
|
||||
self.log.debug(
|
||||
'{}: "{}" created'.format(self.label, data['label'])
|
||||
)
|
||||
|
||||
elif len(matching) == 1:
|
||||
attr_update = matching[0]
|
||||
for key in data:
|
||||
if key not in ['is_hierarchical','entity_type', 'object_type_id']:
|
||||
if (
|
||||
key not in [
|
||||
'is_hierarchical', 'entity_type', 'object_type_id'
|
||||
]
|
||||
):
|
||||
attr_update[key] = data[key]
|
||||
|
||||
self.log.debug(
|
||||
'{}: "{}" updated'.format(self.label, data['label'])
|
||||
)
|
||||
self.session.commit()
|
||||
|
||||
else:
|
||||
raise CustAttrException("Is duplicated")
|
||||
raise CustAttrException('Is duplicated')
|
||||
|
||||
def get_required(self, attr):
|
||||
output = {}
|
||||
for key in self.required_keys:
|
||||
if key not in attr:
|
||||
raise CustAttrException("Key {} is required - please set".format(key))
|
||||
raise CustAttrException(
|
||||
'Key {} is required - please set'.format(key)
|
||||
)
|
||||
|
||||
if attr['type'].lower() not in self.type_posibilities:
|
||||
raise CustAttrException("Type {} is not valid".format(attr['type']))
|
||||
raise CustAttrException(
|
||||
'Type {} is not valid'.format(attr['type'])
|
||||
)
|
||||
|
||||
type_name = attr['type'].lower()
|
||||
|
||||
|
|
@ -338,9 +366,9 @@ class CustomAttributes(BaseAction):
|
|||
|
||||
def get_enumerator_config(self, attr):
|
||||
if 'config' not in attr:
|
||||
raise CustAttrException("Missing config with data")
|
||||
raise CustAttrException('Missing config with data')
|
||||
if 'data' not in attr['config']:
|
||||
raise CustAttrException("Missing data in config")
|
||||
raise CustAttrException('Missing data in config')
|
||||
|
||||
data = []
|
||||
for item in attr['config']['data']:
|
||||
|
|
@ -357,7 +385,7 @@ class CustomAttributes(BaseAction):
|
|||
if isinstance(attr['config'][k], bool):
|
||||
multiSelect = attr['config'][k]
|
||||
else:
|
||||
raise CustAttrException("Multiselect must be boolean")
|
||||
raise CustAttrException('Multiselect must be boolean')
|
||||
break
|
||||
|
||||
config = json.dumps({
|
||||
|
|
@ -393,7 +421,9 @@ class CustomAttributes(BaseAction):
|
|||
return group
|
||||
|
||||
else:
|
||||
raise CustAttrException("Found more than one group '{}'".format(group_name))
|
||||
raise CustAttrException(
|
||||
'Found more than one group "{}"'.format(group_name)
|
||||
)
|
||||
|
||||
def get_role_ALL(self):
|
||||
role_name = 'ALL'
|
||||
|
|
@ -430,8 +460,10 @@ class CustomAttributes(BaseAction):
|
|||
role = self.session.query(query).one()
|
||||
self.security_roles[role_name] = role
|
||||
roles.append(role)
|
||||
except Exception as e:
|
||||
raise CustAttrException("Securit role '{}' does not exist".format(role_name))
|
||||
except Exception:
|
||||
raise CustAttrException(
|
||||
'Securit role "{}" does not exist'.format(role_name)
|
||||
)
|
||||
|
||||
return roles
|
||||
|
||||
|
|
@ -450,12 +482,15 @@ class CustomAttributes(BaseAction):
|
|||
raise CustAttrException('{} boolean'.format(err_msg))
|
||||
elif type == 'enumerator':
|
||||
if not isinstance(default, list):
|
||||
raise CustAttrException('{} array with strings'.format(err_msg))
|
||||
# TODO check if multiSelect is available and if default is one of data menu
|
||||
raise CustAttrException(
|
||||
'{} array with strings'.format(err_msg)
|
||||
)
|
||||
# TODO check if multiSelect is available
|
||||
# and if default is one of data menu
|
||||
if not isinstance(default[0], str):
|
||||
raise CustAttrException('{} array of strings'.format(err_msg))
|
||||
elif type == 'date':
|
||||
date_items = default.split(" ")
|
||||
date_items = default.split(' ')
|
||||
try:
|
||||
if len(date_items) == 1:
|
||||
default = arrow.get(default, 'YY.M.D')
|
||||
|
|
@ -463,7 +498,7 @@ class CustomAttributes(BaseAction):
|
|||
default = arrow.get(default, 'YY.M.D H:m:s')
|
||||
else:
|
||||
raise Exception
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
raise CustAttrException('Date is not in proper format')
|
||||
elif type == 'dynamic enumerator':
|
||||
raise CustAttrException('Dynamic enumerator can\'t have default')
|
||||
|
|
@ -501,7 +536,9 @@ class CustomAttributes(BaseAction):
|
|||
def get_entity_type(self, attr):
|
||||
if 'is_hierarchical' in attr:
|
||||
if attr['is_hierarchical'] is True:
|
||||
type = attr['entity_type'] if ('entity_type' in attr) else 'show'
|
||||
type = 'show'
|
||||
if 'entity_type' in attr:
|
||||
type = attr['entity_type']
|
||||
|
||||
return {
|
||||
'is_hierarchical': True,
|
||||
|
|
@ -520,10 +557,14 @@ class CustomAttributes(BaseAction):
|
|||
object_type_name = attr['object_type']
|
||||
if object_type_name not in self.object_type_ids:
|
||||
try:
|
||||
query = 'ObjectType where name is "{}"'.format(object_type_name)
|
||||
query = 'ObjectType where name is "{}"'.format(
|
||||
object_type_name
|
||||
)
|
||||
object_type_id = self.session.query(query).one()['id']
|
||||
except Exception as e:
|
||||
raise CustAttrException('Object type with name "{}" don\'t exist'.format(object_type_name))
|
||||
except Exception:
|
||||
raise CustAttrException((
|
||||
'Object type with name "{}" don\'t exist'
|
||||
).format(object_type_name))
|
||||
self.object_type_ids[object_type_name] = object_type_id
|
||||
else:
|
||||
object_type_id = self.object_type_ids[object_type_name]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import sys
|
|||
import errno
|
||||
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
import json
|
||||
from pype import api as pype
|
||||
|
||||
|
|
@ -21,7 +21,10 @@ class CreateFolders(BaseAction):
|
|||
label = 'Create Folders'
|
||||
|
||||
#: Action Icon.
|
||||
icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/698620-icon-105-folder-add-512.png'
|
||||
icon = (
|
||||
'https://cdn1.iconfinder.com/data/icons/hawcons/32/'
|
||||
'698620-icon-105-folder-add-512.png'
|
||||
)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
|
@ -120,7 +123,7 @@ class CreateFolders(BaseAction):
|
|||
message = str(ve)
|
||||
self.log.error('Error during syncToAvalon: {}'.format(message))
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
job['status'] = 'failed'
|
||||
session.commit()
|
||||
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import getpass
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
|
||||
|
||||
class VersionsCleanup(BaseAction):
|
||||
|
|
@ -14,7 +13,6 @@ class VersionsCleanup(BaseAction):
|
|||
# Action label
|
||||
label = 'Versions cleanup'
|
||||
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
||||
|
|
@ -34,13 +32,13 @@ class VersionsCleanup(BaseAction):
|
|||
session.delete(version)
|
||||
try:
|
||||
session.commit()
|
||||
except:
|
||||
except Exception:
|
||||
session.rollback()
|
||||
raise
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'removed hidden versions'
|
||||
'message': 'Hidden versions were removed'
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,27 +5,25 @@ import os
|
|||
import re
|
||||
from operator import itemgetter
|
||||
import ftrack_api
|
||||
from app.api import Logger
|
||||
from pype.ftrack import BaseHandler
|
||||
|
||||
|
||||
class DJVViewAction(object):
|
||||
class DJVViewAction(BaseHandler):
|
||||
"""Launch DJVView action."""
|
||||
identifier = "djvview-launch-action"
|
||||
# label = "DJV View"
|
||||
# icon = "http://a.fsdn.com/allura/p/djv/icon"
|
||||
type = 'Application'
|
||||
|
||||
def __init__(self, session):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
|
||||
self.log = Logger.getLogger(self.__class__.__name__)
|
||||
super().__init__(session)
|
||||
|
||||
if self.identifier is None:
|
||||
raise ValueError(
|
||||
'Action missing identifier.'
|
||||
)
|
||||
|
||||
self.session = session
|
||||
|
||||
def is_valid_selection(self, event):
|
||||
selection = event["data"].get("selection", [])
|
||||
|
||||
|
|
@ -75,15 +73,18 @@ class DJVViewAction(object):
|
|||
self.session.api_user
|
||||
), self.discover
|
||||
)
|
||||
|
||||
launch_subscription = (
|
||||
'topic=ftrack.action.launch'
|
||||
' and data.actionIdentifier={0}'
|
||||
' and source.user.username={1}'
|
||||
)
|
||||
self.session.event_hub.subscribe(
|
||||
'topic=ftrack.action.launch and data.actionIdentifier={0} and source.user.username={1}'.format(
|
||||
launch_subscription.format(
|
||||
self.identifier,
|
||||
self.session.api_user
|
||||
),
|
||||
self.launch
|
||||
)
|
||||
self.log.info("----- action - <" + self.__class__.__name__ + "> - Has been registered -----")
|
||||
|
||||
def get_applications(self):
|
||||
applications = []
|
||||
|
|
@ -115,16 +116,17 @@ class DJVViewAction(object):
|
|||
|
||||
if not os.path.exists(start):
|
||||
raise ValueError(
|
||||
'First part "{0}" of expression "{1}" must match exactly to an '
|
||||
'existing entry on the filesystem.'
|
||||
'First part "{0}" of expression "{1}" must match exactly to an'
|
||||
' existing entry on the filesystem.'
|
||||
.format(start, expression)
|
||||
)
|
||||
|
||||
|
||||
expressions = list(map(re.compile, pieces))
|
||||
expressionsCount = len(expression)-1
|
||||
|
||||
for location, folders, files in os.walk(start, topdown=True, followlinks=True):
|
||||
for location, folders, files in os.walk(
|
||||
start, topdown=True, followlinks=True
|
||||
):
|
||||
level = location.rstrip(os.path.sep).count(os.path.sep)
|
||||
expression = expressions[level]
|
||||
|
||||
|
|
@ -158,8 +160,8 @@ class DJVViewAction(object):
|
|||
else:
|
||||
self.logger.debug(
|
||||
'Discovered application executable, but it '
|
||||
'does not appear to o contain required version '
|
||||
'information: {0}'.format(path)
|
||||
'does not appear to o contain required version'
|
||||
' information: {0}'.format(path)
|
||||
)
|
||||
|
||||
# Don't descend any further as out of patterns to match.
|
||||
|
|
@ -175,7 +177,9 @@ class DJVViewAction(object):
|
|||
entities = list()
|
||||
for entity in selection:
|
||||
entities.append(
|
||||
(session.get(self.get_entity_type(entity), entity.get('entityId')))
|
||||
(session.get(
|
||||
self.get_entity_type(entity), entity.get('entityId')
|
||||
))
|
||||
)
|
||||
|
||||
return entities
|
||||
|
|
@ -213,7 +217,7 @@ class DJVViewAction(object):
|
|||
# TODO Is this proper way?
|
||||
try:
|
||||
fps = int(entities[0]['custom_attributes']['fps'])
|
||||
except:
|
||||
except Exception:
|
||||
fps = 24
|
||||
|
||||
# TODO issequence is probably already built-in validation in ftrack
|
||||
|
|
@ -239,29 +243,46 @@ class DJVViewAction(object):
|
|||
range = (padding % start) + '-' + (padding % end)
|
||||
filename = re.sub('%[0-9]*d', range, filename)
|
||||
else:
|
||||
msg = (
|
||||
'DJV View - Filename has more than one'
|
||||
' sequence identifier.'
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'DJV View - Filename has more than one seqence identifier.'
|
||||
'message': (msg)
|
||||
}
|
||||
|
||||
cmd = []
|
||||
# DJV path
|
||||
cmd.append(os.path.normpath(self.djv_path))
|
||||
# DJV Options Start ##############################################
|
||||
# cmd.append('-file_layer (value)') #layer name
|
||||
cmd.append('-file_proxy 1/2') # Proxy scale: 1/2, 1/4, 1/8
|
||||
cmd.append('-file_cache True') # Cache: True, False.
|
||||
# cmd.append('-window_fullscreen') #Start in full screen
|
||||
# cmd.append("-window_toolbar False") # Toolbar controls: False, True.
|
||||
# cmd.append("-window_playbar False") # Window controls: False, True.
|
||||
# cmd.append("-view_grid None") # Grid overlay: None, 1x1, 10x10, 100x100.
|
||||
# cmd.append("-view_hud True") # Heads up display: True, False.
|
||||
cmd.append("-playback Forward") # Playback: Stop, Forward, Reverse.
|
||||
# cmd.append("-playback_frame (value)") # Frame.
|
||||
'''layer name'''
|
||||
# cmd.append('-file_layer (value)')
|
||||
''' Proxy scale: 1/2, 1/4, 1/8'''
|
||||
cmd.append('-file_proxy 1/2')
|
||||
''' Cache: True, False.'''
|
||||
cmd.append('-file_cache True')
|
||||
''' Start in full screen '''
|
||||
# cmd.append('-window_fullscreen')
|
||||
''' Toolbar controls: False, True.'''
|
||||
# cmd.append("-window_toolbar False")
|
||||
''' Window controls: False, True.'''
|
||||
# cmd.append("-window_playbar False")
|
||||
''' Grid overlay: None, 1x1, 10x10, 100x100.'''
|
||||
# cmd.append("-view_grid None")
|
||||
''' Heads up display: True, False.'''
|
||||
# cmd.append("-view_hud True")
|
||||
''' Playback: Stop, Forward, Reverse.'''
|
||||
cmd.append("-playback Forward")
|
||||
''' Frame.'''
|
||||
# cmd.append("-playback_frame (value)")
|
||||
cmd.append("-playback_speed " + str(fps))
|
||||
# cmd.append("-playback_timer (value)") # Timer: Sleep, Timeout. Value: Sleep.
|
||||
# cmd.append("-playback_timer_resolution (value)") # Timer resolution (seconds): 0.001.
|
||||
cmd.append("-time_units Frames") # Time units: Timecode, Frames.
|
||||
''' Timer: Sleep, Timeout. Value: Sleep.'''
|
||||
# cmd.append("-playback_timer (value)")
|
||||
''' Timer resolution (seconds): 0.001.'''
|
||||
# cmd.append("-playback_timer_resolution (value)")
|
||||
''' Time units: Timecode, Frames.'''
|
||||
cmd.append("-time_units Frames")
|
||||
# DJV Options End ################################################
|
||||
|
||||
# PATH TO COMPONENT
|
||||
|
|
@ -287,7 +308,7 @@ class DJVViewAction(object):
|
|||
if entity['components'][0]['file_type'] in allowed_types:
|
||||
versions.append(entity)
|
||||
|
||||
if entity.entity_type.lower() == "task":
|
||||
elif entity.entity_type.lower() == "task":
|
||||
# AssetVersions are obtainable only from shot!
|
||||
shotentity = entity['parent']
|
||||
|
||||
|
|
@ -297,7 +318,8 @@ class DJVViewAction(object):
|
|||
if version['task']['id'] != entity['id']:
|
||||
continue
|
||||
# Get only components with allowed type
|
||||
if version['components'][0]['file_type'] in allowed_types:
|
||||
filetype = version['components'][0]['file_type']
|
||||
if filetype in allowed_types:
|
||||
versions.append(version)
|
||||
|
||||
# Raise error if no components were found
|
||||
|
|
@ -317,15 +339,21 @@ class DJVViewAction(object):
|
|||
try:
|
||||
# TODO This is proper way to get filepath!!!
|
||||
# THIS WON'T WORK RIGHT NOW
|
||||
location = component['component_locations'][0]['location']
|
||||
location = component[
|
||||
'component_locations'
|
||||
][0]['location']
|
||||
file_path = location.get_filesystem_path(component)
|
||||
# if component.isSequence():
|
||||
# if component.getMembers():
|
||||
# frame = int(component.getMembers()[0].getName())
|
||||
# frame = int(
|
||||
# component.getMembers()[0].getName()
|
||||
# )
|
||||
# file_path = file_path % frame
|
||||
except:
|
||||
except Exception:
|
||||
# This works but is NOT proper way
|
||||
file_path = component['component_locations'][0]['resource_identifier']
|
||||
file_path = component[
|
||||
'component_locations'
|
||||
][0]['resource_identifier']
|
||||
|
||||
event["data"]["items"].append(
|
||||
{"label": label, "value": file_path}
|
||||
|
|
@ -353,7 +381,7 @@ class DJVViewAction(object):
|
|||
}
|
||||
|
||||
|
||||
def register(session, **kw):
|
||||
def register(session):
|
||||
"""Register hooks."""
|
||||
if not isinstance(session, ftrack_api.session.Session):
|
||||
return
|
||||
|
|
@ -367,6 +395,7 @@ def main(arguments=None):
|
|||
if arguments is None:
|
||||
arguments = []
|
||||
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
# Allow setting of logging level from arguments.
|
||||
loggingLevels = {}
|
||||
|
|
@ -4,48 +4,89 @@ import sys
|
|||
import argparse
|
||||
import logging
|
||||
|
||||
import datetime
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
|
||||
|
||||
class JobKiller(BaseAction):
|
||||
'''Edit meta data action.'''
|
||||
|
||||
#: Action identifier.
|
||||
identifier = 'job.kill'
|
||||
identifier = 'job.killer'
|
||||
#: Action label.
|
||||
label = 'Job Killer'
|
||||
#: Action description.
|
||||
description = 'Killing all running jobs younger than day'
|
||||
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
||||
return True
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
if not event['data'].get('values', {}):
|
||||
title = 'Select jobs to kill'
|
||||
|
||||
jobs = session.query(
|
||||
'select id, status from Job'
|
||||
' where status in ("queued", "running")'
|
||||
)
|
||||
|
||||
items = []
|
||||
import json
|
||||
for job in jobs:
|
||||
data = json.loads(job['data'])
|
||||
user = job['user']['username']
|
||||
created = job['created_at'].strftime('%d.%m.%Y %H:%M:%S')
|
||||
label = '{}/ {}/ {}'.format(
|
||||
data['description'], created, user
|
||||
)
|
||||
item = {
|
||||
'label': label,
|
||||
'name': job['id'],
|
||||
'type': 'boolean',
|
||||
'value': False
|
||||
}
|
||||
items.append(item)
|
||||
|
||||
return {
|
||||
'items': items,
|
||||
'title': title
|
||||
}
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
""" GET JOB """
|
||||
if 'values' not in event['data']:
|
||||
return
|
||||
|
||||
yesterday = datetime.date.today() - datetime.timedelta(days=1)
|
||||
values = event['data']['values']
|
||||
if len(values) <= 0:
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'No jobs to kill!'
|
||||
}
|
||||
jobs = []
|
||||
job_ids = []
|
||||
|
||||
jobs = session.query(
|
||||
'select id, status from Job '
|
||||
'where status in ("queued", "running") and created_at > {0}'.format(yesterday)
|
||||
)
|
||||
for k, v in values.items():
|
||||
if v is True:
|
||||
job_ids.append(k)
|
||||
|
||||
for id in job_ids:
|
||||
query = 'Job where id is "{}"'.format(id)
|
||||
jobs.append(session.query(query).one())
|
||||
# Update all the queried jobs, setting the status to failed.
|
||||
for job in jobs:
|
||||
self.log.debug(job['created_at'])
|
||||
self.log.debug('Changing Job ({}) status: {} -> failed'.format(job['id'], job['status']))
|
||||
job['status'] = 'failed'
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except:
|
||||
session.rollback()
|
||||
try:
|
||||
job['status'] = 'failed'
|
||||
session.commit()
|
||||
self.log.debug((
|
||||
'Changing Job ({}) status: {} -> failed'
|
||||
).format(job['id'], job['status']))
|
||||
except Exception:
|
||||
self.warning.debug((
|
||||
'Changing Job ({}) has failed'
|
||||
).format(job['id']))
|
||||
|
||||
self.log.info('All running jobs were killed Successfully!')
|
||||
return {
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import getpass
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
|
||||
|
||||
class SetVersion(BaseAction):
|
||||
|
|
@ -11,11 +10,9 @@ class SetVersion(BaseAction):
|
|||
|
||||
#: Action identifier.
|
||||
identifier = 'version.set'
|
||||
|
||||
#: Action label.
|
||||
label = 'Version Set'
|
||||
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
||||
|
|
@ -49,23 +46,24 @@ class SetVersion(BaseAction):
|
|||
# Do something with the values or return a new form.
|
||||
values = event['data'].get('values', {})
|
||||
# Default is action True
|
||||
scs = True
|
||||
msg = 'Version was changed to v{0}'.format(values['version_number'])
|
||||
scs = False
|
||||
|
||||
if not values['version_number']:
|
||||
scs = False,
|
||||
msg = "You didn't enter any version."
|
||||
msg = 'You didn\'t enter any version.'
|
||||
elif int(values['version_number']) <= 0:
|
||||
scs = False
|
||||
msg = 'Negative or zero version is not valid.'
|
||||
else:
|
||||
entity['version'] = values['version_number']
|
||||
|
||||
try:
|
||||
session.commit()
|
||||
except:
|
||||
session.rollback()
|
||||
raise
|
||||
try:
|
||||
entity['version'] = values['version_number']
|
||||
session.commit()
|
||||
msg = 'Version was changed to v{0}'.format(
|
||||
values['version_number']
|
||||
)
|
||||
scs = True
|
||||
except Exception as e:
|
||||
msg = 'Unexpected error occurs during version set ({})'.format(
|
||||
str(e)
|
||||
)
|
||||
|
||||
return {
|
||||
'success': scs,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,9 @@ import sys
|
|||
import argparse
|
||||
import logging
|
||||
import json
|
||||
import importlib
|
||||
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import ftrack_utils
|
||||
from pype.ftrack import BaseAction, lib as ftracklib
|
||||
|
||||
|
||||
class SyncToAvalon(BaseAction):
|
||||
|
|
@ -56,11 +54,12 @@ class SyncToAvalon(BaseAction):
|
|||
'https://cdn1.iconfinder.com/data/icons/hawcons/32/'
|
||||
'699650-icon-92-inbox-download-512.png'
|
||||
)
|
||||
#: Action priority
|
||||
priority = 200
|
||||
|
||||
def __init__(self, session):
|
||||
super(SyncToAvalon, self).__init__(session)
|
||||
# reload utils on initialize (in case of server restart)
|
||||
importlib.reload(ftrack_utils)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
|
@ -118,12 +117,12 @@ class SyncToAvalon(BaseAction):
|
|||
all_names = []
|
||||
duplicates = []
|
||||
|
||||
for e in self.importable:
|
||||
ftrack_utils.avalon_check_name(e)
|
||||
if e['name'] in all_names:
|
||||
for entity in self.importable:
|
||||
ftracklib.avalon_check_name(entity)
|
||||
if entity['name'] in all_names:
|
||||
duplicates.append("'{}'".format(e['name']))
|
||||
else:
|
||||
all_names.append(e['name'])
|
||||
all_names.append(entity['name'])
|
||||
|
||||
if len(duplicates) > 0:
|
||||
raise ValueError(
|
||||
|
|
@ -133,12 +132,12 @@ class SyncToAvalon(BaseAction):
|
|||
# ----- PROJECT ------
|
||||
# store Ftrack project- self.importable[0] must be project entity!!
|
||||
ft_project = self.importable[0]
|
||||
avalon_project = ftrack_utils.get_avalon_project(ft_project)
|
||||
custom_attributes = ftrack_utils.get_avalon_attr(session)
|
||||
avalon_project = ftracklib.get_avalon_project(ft_project)
|
||||
custom_attributes = ftracklib.get_avalon_attr(session)
|
||||
|
||||
# Import all entities to Avalon DB
|
||||
for entity in self.importable:
|
||||
result = ftrack_utils.import_to_avalon(
|
||||
result = ftracklib.import_to_avalon(
|
||||
session=session,
|
||||
entity=entity,
|
||||
ft_project=ft_project,
|
||||
|
|
@ -177,18 +176,15 @@ class SyncToAvalon(BaseAction):
|
|||
avalon_project = result['project']
|
||||
|
||||
job['status'] = 'done'
|
||||
session.commit()
|
||||
self.log.info('Synchronization to Avalon was successfull!')
|
||||
|
||||
except ValueError as ve:
|
||||
job['status'] = 'failed'
|
||||
session.commit()
|
||||
message = str(ve)
|
||||
self.log.error('Error during syncToAvalon: {}'.format(message))
|
||||
|
||||
except Exception as e:
|
||||
job['status'] = 'failed'
|
||||
session.commit()
|
||||
exc_type, exc_obj, exc_tb = sys.exc_info()
|
||||
fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
|
||||
log_message = "{}/{}/Line: {}".format(
|
||||
|
|
@ -201,6 +197,10 @@ class SyncToAvalon(BaseAction):
|
|||
'Unexpected Error'
|
||||
' - Please check Log for more information'
|
||||
)
|
||||
finally:
|
||||
if job['status'] in ['queued', 'running']:
|
||||
job['status'] = 'failed'
|
||||
session.commit()
|
||||
|
||||
if len(message) > 0:
|
||||
message = "Unable to sync: {}".format(message)
|
||||
|
|
@ -235,7 +235,7 @@ def register(session, **kw):
|
|||
return
|
||||
|
||||
action_handler = SyncToAvalon(session)
|
||||
action_handler.register(200)
|
||||
action_handler.register()
|
||||
|
||||
|
||||
def main(arguments=None):
|
||||
|
|
|
|||
|
|
@ -9,10 +9,13 @@ import json
|
|||
import re
|
||||
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
from avalon import io, inventory, schema
|
||||
|
||||
|
||||
ignore_me = True
|
||||
|
||||
|
||||
class TestAction(BaseAction):
|
||||
'''Edit meta data action.'''
|
||||
|
||||
|
|
@ -22,6 +25,8 @@ class TestAction(BaseAction):
|
|||
label = 'Test action'
|
||||
#: Action description.
|
||||
description = 'Test action'
|
||||
#: priority
|
||||
priority = 10000
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
|
@ -38,7 +43,7 @@ class TestAction(BaseAction):
|
|||
return discover
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
entity = entities[0]
|
||||
self.log.info(event)
|
||||
|
||||
return True
|
||||
|
||||
|
|
@ -50,7 +55,7 @@ def register(session, **kw):
|
|||
return
|
||||
|
||||
action_handler = TestAction(session)
|
||||
action_handler.register(10000)
|
||||
action_handler.register()
|
||||
|
||||
|
||||
def main(arguments=None):
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import getpass
|
||||
import json
|
||||
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
|
||||
|
||||
class ThumbToChildren(BaseAction):
|
||||
'''Custom action.'''
|
||||
|
|
@ -18,8 +18,10 @@ class ThumbToChildren(BaseAction):
|
|||
# Action label
|
||||
label = 'Thumbnail to Children'
|
||||
# Action icon
|
||||
icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239322-download_transfer-128.png"
|
||||
|
||||
icon = (
|
||||
'https://cdn3.iconfinder.com/data/icons/transfers/100/'
|
||||
'239322-download_transfer-128.png'
|
||||
)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
|
@ -29,7 +31,6 @@ class ThumbToChildren(BaseAction):
|
|||
|
||||
return True
|
||||
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
'''Callback method for action.'''
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ class ThumbToChildren(BaseAction):
|
|||
|
||||
# inform the user that the job is done
|
||||
job['status'] = 'done'
|
||||
except:
|
||||
except Exception:
|
||||
# fail the job if something goes wrong
|
||||
job['status'] = 'failed'
|
||||
raise
|
||||
|
|
@ -66,7 +67,6 @@ class ThumbToChildren(BaseAction):
|
|||
}
|
||||
|
||||
|
||||
|
||||
def register(session, **kw):
|
||||
'''Register action. Called when used as an event plugin.'''
|
||||
if not isinstance(session, ftrack_api.session.Session):
|
||||
|
|
@ -75,6 +75,7 @@ def register(session, **kw):
|
|||
action_handler = ThumbToChildren(session)
|
||||
action_handler.register()
|
||||
|
||||
|
||||
def main(arguments=None):
|
||||
'''Set up logging and register action.'''
|
||||
if arguments is None:
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@
|
|||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import getpass
|
||||
import json
|
||||
import ftrack_api
|
||||
from ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction
|
||||
|
||||
|
||||
class ThumbToParent(BaseAction):
|
||||
'''Custom action.'''
|
||||
|
|
@ -17,8 +17,10 @@ class ThumbToParent(BaseAction):
|
|||
# Action label
|
||||
label = 'Thumbnail to Parent'
|
||||
# Action icon
|
||||
icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239419-upload_transfer-512.png"
|
||||
|
||||
icon = (
|
||||
"https://cdn3.iconfinder.com/data/icons/transfers/100/"
|
||||
"239419-upload_transfer-512.png"
|
||||
)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
'''Return action config if triggered on asset versions.'''
|
||||
|
|
@ -28,7 +30,6 @@ class ThumbToParent(BaseAction):
|
|||
|
||||
return True
|
||||
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
'''Callback method for action.'''
|
||||
|
||||
|
|
@ -50,14 +51,19 @@ class ThumbToParent(BaseAction):
|
|||
if entity.entity_type.lower() == 'assetversion':
|
||||
try:
|
||||
parent = entity['task']
|
||||
except:
|
||||
except Exception:
|
||||
par_ent = entity['link'][-2]
|
||||
parent = session.get(par_ent['type'], par_ent['id'])
|
||||
else:
|
||||
try:
|
||||
parent = entity['parent']
|
||||
except:
|
||||
self.log.error("Durin Action 'Thumb to Parent' went something wrong")
|
||||
except Exception as e:
|
||||
msg = (
|
||||
"Durin Action 'Thumb to Parent'"
|
||||
" went something wrong"
|
||||
)
|
||||
self.log.error(msg)
|
||||
raise e
|
||||
thumbid = entity['thumbnail_id']
|
||||
|
||||
if parent and thumbid:
|
||||
|
|
@ -69,10 +75,10 @@ class ThumbToParent(BaseAction):
|
|||
# inform the user that the job is done
|
||||
job['status'] = status or 'done'
|
||||
|
||||
except:
|
||||
except Exception as e:
|
||||
# fail the job if something goes wrong
|
||||
job['status'] = 'failed'
|
||||
raise
|
||||
raise e
|
||||
|
||||
finally:
|
||||
session.commit()
|
||||
|
|
@ -91,6 +97,7 @@ def register(session, **kw):
|
|||
action_handler = ThumbToParent(session)
|
||||
action_handler.register()
|
||||
|
||||
|
||||
def main(arguments=None):
|
||||
'''Set up logging and register action.'''
|
||||
if arguments is None:
|
||||
|
|
|
|||
|
|
@ -1,430 +0,0 @@
|
|||
import os
|
||||
import operator
|
||||
import ftrack_api
|
||||
import collections
|
||||
import sys
|
||||
import json
|
||||
import base64
|
||||
|
||||
|
||||
ignore_me = True
|
||||
# sys.path.append(os.path.dirname(os.path.dirname(__file__)))
|
||||
# from ftrack_kredenc.lucidity.vendor import yaml
|
||||
# from ftrack_kredenc import lucidity
|
||||
#
|
||||
#
|
||||
# def get_ftrack_connect_path():
|
||||
#
|
||||
# ftrack_connect_root = os.path.abspath(os.getenv('FTRACK_CONNECT_PACKAGE'))
|
||||
#
|
||||
# return ftrack_connect_root
|
||||
#
|
||||
#
|
||||
# def from_yaml(filepath):
|
||||
# ''' Parse a Schema from a YAML file at the given *filepath*.
|
||||
# '''
|
||||
# with open(filepath, 'r') as f:
|
||||
# data = yaml.safe_load(f)
|
||||
# return data
|
||||
#
|
||||
#
|
||||
# def get_task_enviro(entity, environment=None):
|
||||
#
|
||||
# context = get_context(entity)
|
||||
#
|
||||
# if not environment:
|
||||
# environment = {}
|
||||
#
|
||||
# for key in context:
|
||||
# os.environ[key.upper()] = context[key]['name']
|
||||
# environment[key.upper()] = context[key]['name']
|
||||
#
|
||||
# if key == 'Project':
|
||||
# os.putenv('PROJECT_ROOT', context[key]['root'])
|
||||
# os.environ['PROJECT_ROOT'] = context[key]['root']
|
||||
# environment['PROJECT_ROOT'] = context[key]['root']
|
||||
# print('PROJECT_ROOT: ' + context[key]['root'])
|
||||
# print(key + ': ' + context[key]['name'])
|
||||
#
|
||||
# return environment
|
||||
#
|
||||
#
|
||||
# def get_entity():
|
||||
# decodedEventData = json.loads(
|
||||
# base64.b64decode(
|
||||
# os.environ.get('FTRACK_CONNECT_EVENT')
|
||||
# )
|
||||
# )
|
||||
#
|
||||
# entity = decodedEventData.get('selection')[0]
|
||||
#
|
||||
# if entity['entityType'] == 'task':
|
||||
# return ftrack_api.Task(entity['entityId'])
|
||||
# else:
|
||||
# return None
|
||||
#
|
||||
#
|
||||
# def set_env_vars():
|
||||
#
|
||||
# entity = get_entity()
|
||||
#
|
||||
# if entity:
|
||||
# if not os.environ.get('project_root'):
|
||||
# enviro = get_task_enviro(entity)
|
||||
#
|
||||
# print(enviro)
|
||||
#
|
||||
#
|
||||
def get_context(entity):
|
||||
|
||||
entityName = entity['name']
|
||||
entityId = entity['id']
|
||||
entityType = entity.entity_type
|
||||
entityDescription = entity['description']
|
||||
|
||||
print(100*"*")
|
||||
for k in entity['ancestors']:
|
||||
print(k['name'])
|
||||
print(100*"*")
|
||||
hierarchy = entity.getParents()
|
||||
|
||||
ctx = collections.OrderedDict()
|
||||
|
||||
if entity.get('entityType') == 'task' and entityType == 'Task':
|
||||
taskType = entity.getType().getName()
|
||||
entityDic = {
|
||||
'type': taskType,
|
||||
'name': entityName,
|
||||
'id': entityId,
|
||||
'description': entityDescription
|
||||
}
|
||||
elif entity.get('entityType') == 'task':
|
||||
entityDic = {
|
||||
'name': entityName,
|
||||
'id': entityId,
|
||||
'description': entityDescription
|
||||
}
|
||||
|
||||
ctx[entityType] = entityDic
|
||||
|
||||
folder_counter = 0
|
||||
|
||||
for ancestor in hierarchy:
|
||||
tempdic = {}
|
||||
if isinstance(ancestor, ftrack_api.Component):
|
||||
# Ignore intermediate components.
|
||||
continue
|
||||
|
||||
tempdic['name'] = ancestor.getName()
|
||||
tempdic['id'] = ancestor.getId()
|
||||
|
||||
try:
|
||||
objectType = ancestor.getObjectType()
|
||||
tempdic['description'] = ancestor.getDescription()
|
||||
except AttributeError:
|
||||
objectType = 'Project'
|
||||
tempdic['description'] = ''
|
||||
|
||||
if objectType == 'Asset Build':
|
||||
tempdic['type'] = ancestor.getType().get('name')
|
||||
objectType = objectType.replace(' ', '_')
|
||||
elif objectType == 'Project':
|
||||
tempdic['code'] = tempdic['name']
|
||||
tempdic['name'] = ancestor.get('fullname')
|
||||
tempdic['root'] = ancestor.getRoot()
|
||||
|
||||
if objectType == 'Folder':
|
||||
objectType = objectType + str(folder_counter)
|
||||
folder_counter += 1
|
||||
ctx[objectType] = tempdic
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
def getNewContext(entity):
|
||||
|
||||
parents = []
|
||||
item = entity
|
||||
while True:
|
||||
item = item['parent']
|
||||
if not item:
|
||||
break
|
||||
parents.append(item)
|
||||
|
||||
ctx = collections.OrderedDict()
|
||||
|
||||
entityDic = {
|
||||
'name': entity['name'],
|
||||
'id': entity['id'],
|
||||
}
|
||||
try:
|
||||
entityDic['type'] = entity['type']['name']
|
||||
except:
|
||||
pass
|
||||
|
||||
ctx[entity['object_type']['name']] = entityDic
|
||||
|
||||
print(100*"-")
|
||||
for p in parents:
|
||||
print(p)
|
||||
# add all parents to the context
|
||||
for parent in parents:
|
||||
tempdic = {}
|
||||
if not parent.get('project_schema'):
|
||||
tempdic = {
|
||||
'name': parent['full_name'],
|
||||
'code': parent['name'],
|
||||
'id': parent['id'],
|
||||
}
|
||||
tempdic = {
|
||||
'name': parent['name'],
|
||||
'id': parent['id'],
|
||||
}
|
||||
object_type = parent['object_type']['name']
|
||||
|
||||
ctx[object_type] = tempdic
|
||||
|
||||
# add project to the context
|
||||
project = entity['project']
|
||||
ctx['Project'] = {
|
||||
'name': project['full_name'],
|
||||
'code': project['name'],
|
||||
'id': project['id'],
|
||||
'root': project['root'],
|
||||
},
|
||||
|
||||
return ctx
|
||||
#
|
||||
#
|
||||
# def get_frame_range():
|
||||
#
|
||||
# entity = get_entity()
|
||||
# entityType = entity.getObjectType()
|
||||
# environment = {}
|
||||
#
|
||||
# if entityType == 'Task':
|
||||
# try:
|
||||
# environment['FS'] = str(int(entity.getFrameStart()))
|
||||
# except Exception:
|
||||
# environment['FS'] = '1'
|
||||
# try:
|
||||
# environment['FE'] = str(int(entity.getFrameEnd()))
|
||||
# except Exception:
|
||||
# environment['FE'] = '1'
|
||||
# else:
|
||||
# try:
|
||||
# environment['FS'] = str(int(entity.getFrameStart()))
|
||||
# except Exception:
|
||||
# environment['FS'] = '1'
|
||||
# try:
|
||||
# environment['FE'] = str(int(entity.getFrameEnd()))
|
||||
# except Exception:
|
||||
# environment['FE'] = '1'
|
||||
#
|
||||
#
|
||||
# def get_asset_name_by_id(id):
|
||||
# for t in ftrack_api.getAssetTypes():
|
||||
# try:
|
||||
# if t.get('typeid') == id:
|
||||
# return t.get('name')
|
||||
# except:
|
||||
# return None
|
||||
#
|
||||
#
|
||||
# def get_status_by_name(name):
|
||||
# statuses = ftrack_api.getTaskStatuses()
|
||||
#
|
||||
# result = None
|
||||
# for s in statuses:
|
||||
# if s.get('name').lower() == name.lower():
|
||||
# result = s
|
||||
#
|
||||
# return result
|
||||
#
|
||||
#
|
||||
# def sort_types(types):
|
||||
# data = {}
|
||||
# for t in types:
|
||||
# data[t] = t.get('sort')
|
||||
#
|
||||
# data = sorted(data.items(), key=operator.itemgetter(1))
|
||||
# results = []
|
||||
# for item in data:
|
||||
# results.append(item[0])
|
||||
#
|
||||
# return results
|
||||
#
|
||||
#
|
||||
# def get_next_task(task):
|
||||
# shot = task.getParent()
|
||||
# tasks = shot.getTasks()
|
||||
#
|
||||
# types_sorted = sort_types(ftrack_api.getTaskTypes())
|
||||
#
|
||||
# next_types = None
|
||||
# for t in types_sorted:
|
||||
# if t.get('typeid') == task.get('typeid'):
|
||||
# try:
|
||||
# next_types = types_sorted[(types_sorted.index(t) + 1):]
|
||||
# except:
|
||||
# pass
|
||||
#
|
||||
# for nt in next_types:
|
||||
# for t in tasks:
|
||||
# if nt.get('typeid') == t.get('typeid'):
|
||||
# return t
|
||||
#
|
||||
# return None
|
||||
#
|
||||
#
|
||||
# def get_latest_version(versions):
|
||||
# latestVersion = None
|
||||
# if len(versions) > 0:
|
||||
# versionNumber = 0
|
||||
# for item in versions:
|
||||
# if item.get('version') > versionNumber:
|
||||
# versionNumber = item.getVersion()
|
||||
# latestVersion = item
|
||||
# return latestVersion
|
||||
#
|
||||
#
|
||||
# def get_thumbnail_recursive(task):
|
||||
# if task.get('thumbid'):
|
||||
# thumbid = task.get('thumbid')
|
||||
# return ftrack_api.Attachment(id=thumbid)
|
||||
# if not task.get('thumbid'):
|
||||
# parent = ftrack_api.Task(id=task.get('parent_id'))
|
||||
# return get_thumbnail_recursive(parent)
|
||||
#
|
||||
#
|
||||
# # paths_collected
|
||||
#
|
||||
# def getFolderHierarchy(context):
|
||||
# '''Return structure for *hierarchy*.
|
||||
# '''
|
||||
#
|
||||
# hierarchy = []
|
||||
# for key in reversed(context):
|
||||
# hierarchy.append(context[key]['name'])
|
||||
# print(hierarchy)
|
||||
#
|
||||
# return os.path.join(*hierarchy[1:-1])
|
||||
#
|
||||
#
|
||||
def tweakContext(context, include=False):
|
||||
|
||||
for key in context:
|
||||
if key == 'Asset Build':
|
||||
context['Asset_Build'] = context.pop(key)
|
||||
key = 'Asset_Build'
|
||||
description = context[key].get('description')
|
||||
if description:
|
||||
context[key]['description'] = '_' + description
|
||||
|
||||
hierarchy = []
|
||||
for key in reversed(context):
|
||||
hierarchy.append(context[key]['name'])
|
||||
|
||||
if include:
|
||||
hierarchy = os.path.join(*hierarchy[1:])
|
||||
else:
|
||||
hierarchy = os.path.join(*hierarchy[1:-1])
|
||||
|
||||
context['ft_hierarchy'] = hierarchy
|
||||
|
||||
|
||||
def getSchema(entity):
|
||||
|
||||
project = entity['project']
|
||||
schema = project['project_schema']['name']
|
||||
|
||||
tools = os.path.abspath(os.environ.get('studio_tools'))
|
||||
|
||||
schema_path = os.path.join(tools, 'studio', 'templates', (schema + '_' + project['name'] + '.yml'))
|
||||
if not os.path.exists(schema_path):
|
||||
schema_path = os.path.join(tools, 'studio', 'templates', (schema + '.yml'))
|
||||
if not os.path.exists(schema_path):
|
||||
schema_path = os.path.join(tools, 'studio', 'templates', 'default.yml')
|
||||
|
||||
schema = lucidity.Schema.from_yaml(schema_path)
|
||||
|
||||
print(schema_path)
|
||||
return schema
|
||||
|
||||
|
||||
# def getAllPathsYaml(entity, root=''):
|
||||
#
|
||||
# if isinstance(entity, str) or isinstance(entity, unicode):
|
||||
# entity = ftrack_api.Task(entity)
|
||||
#
|
||||
# context = get_context(entity)
|
||||
#
|
||||
# tweakContext(context)
|
||||
#
|
||||
# schema = getSchema(entity)
|
||||
#
|
||||
# paths = schema.format_all(context)
|
||||
# paths_collected = []
|
||||
#
|
||||
# for path in paths:
|
||||
# tweak_path = path[0].replace(" ", '_').replace('\'', '').replace('\\', '/')
|
||||
#
|
||||
# tempPath = os.path.join(root, tweak_path)
|
||||
# path = list(path)
|
||||
# path[0] = tempPath
|
||||
# paths_collected.append(path)
|
||||
#
|
||||
# return paths_collected
|
||||
#
|
||||
|
||||
def getPathsYaml(entity, templateList=None, root=None, **kwargs):
|
||||
'''
|
||||
version=None
|
||||
ext=None
|
||||
item=None
|
||||
family=None
|
||||
subset=None
|
||||
'''
|
||||
|
||||
context = get_context(entity)
|
||||
|
||||
if entity.entity_type != 'Task':
|
||||
tweakContext(context, include=True)
|
||||
else:
|
||||
tweakContext(context)
|
||||
|
||||
context.update(kwargs)
|
||||
|
||||
host = sys.executable.lower()
|
||||
|
||||
ext = None
|
||||
if not context.get('ext'):
|
||||
if "nuke" in host:
|
||||
ext = 'nk'
|
||||
elif "maya" in host:
|
||||
ext = 'ma'
|
||||
elif "houdini" in host:
|
||||
ext = 'hip'
|
||||
if ext:
|
||||
context['ext'] = ext
|
||||
|
||||
if not context.get('subset'):
|
||||
context['subset'] = ''
|
||||
else:
|
||||
context['subset'] = '_' + context['subset']
|
||||
|
||||
schema = getSchema(entity)
|
||||
paths = schema.format_all(context)
|
||||
paths_collected = set([])
|
||||
for temp_mask in templateList:
|
||||
for path in paths:
|
||||
if temp_mask in path[1].name:
|
||||
path = path[0].lower().replace(" ", '_').replace('\'', '').replace('\\', '/')
|
||||
path_list = path.split('/')
|
||||
if path_list[0].endswith(':'):
|
||||
path_list[0] = path_list[0] + os.path.sep
|
||||
path = os.path.join(*path_list)
|
||||
temppath = os.path.join(root, path)
|
||||
paths_collected.add(temppath)
|
||||
|
||||
return list(paths_collected)
|
||||
|
|
@ -1,783 +0,0 @@
|
|||
# :coding: utf-8
|
||||
# :copyright: Copyright (c) 2017 ftrack
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import ftrack_api
|
||||
from avalon import lib
|
||||
import acre
|
||||
from pype.ftrack import ftrack_utils
|
||||
|
||||
from pype.ftrack import ftrack_utils
|
||||
from pype import api as pype
|
||||
|
||||
|
||||
ignore_me = True
|
||||
|
||||
|
||||
class AppAction(object):
|
||||
'''Custom Action base class
|
||||
|
||||
<label> - a descriptive string identifing your action.
|
||||
<varaint> - To group actions together, give them the same
|
||||
label and specify a unique variant per action.
|
||||
<identifier> - a unique identifier for app.
|
||||
<description> - a verbose descriptive text for you action
|
||||
<icon> - icon in ftrack
|
||||
'''
|
||||
|
||||
def __init__(
|
||||
self, session, label, name, executable,
|
||||
variant=None, icon=None, description=None
|
||||
):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
|
||||
self.log = pype.Logger.getLogger(self.__class__.__name__)
|
||||
|
||||
# self.logger = Logger.getLogger(__name__)
|
||||
|
||||
if label is None:
|
||||
raise ValueError('Action missing label.')
|
||||
elif name is None:
|
||||
raise ValueError('Action missing identifier.')
|
||||
elif executable is None:
|
||||
raise ValueError('Action missing executable.')
|
||||
|
||||
self._session = session
|
||||
self.label = label
|
||||
self.identifier = name
|
||||
self.executable = executable
|
||||
self.variant = variant
|
||||
self.icon = icon
|
||||
self.description = description
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
'''Return current session.'''
|
||||
return self._session
|
||||
|
||||
def register(self, priority=100):
|
||||
'''Registers the action, subscribing the discover and launch topics.'''
|
||||
|
||||
discovery_subscription = (
|
||||
'topic=ftrack.action.discover and source.user.username={0}'
|
||||
).format(self.session.api_user)
|
||||
|
||||
self.session.event_hub.subscribe(
|
||||
discovery_subscription,
|
||||
self._discover,
|
||||
priority=priority
|
||||
)
|
||||
|
||||
launch_subscription = (
|
||||
'topic=ftrack.action.launch'
|
||||
' and data.actionIdentifier={0}'
|
||||
' and source.user.username={1}'
|
||||
).format(
|
||||
self.identifier,
|
||||
self.session.api_user
|
||||
)
|
||||
self.session.event_hub.subscribe(
|
||||
launch_subscription,
|
||||
self._launch
|
||||
)
|
||||
self.log.info((
|
||||
"Application '{} {}' - Registered successfully"
|
||||
).format(self.label, self.variant))
|
||||
|
||||
def _discover(self, event):
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
accepts = self.discover(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
if accepts:
|
||||
self.log.debug('Selection is valid')
|
||||
return {
|
||||
'items': [{
|
||||
'label': self.label,
|
||||
'variant': self.variant,
|
||||
'description': self.description,
|
||||
'actionIdentifier': self.identifier,
|
||||
'icon': self.icon,
|
||||
}]
|
||||
}
|
||||
else:
|
||||
self.log.debug('Selection is _not_ valid')
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
'''Return true if we can handle the selected entities.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and
|
||||
the entity id. If the entity is a hierarchical you will always get
|
||||
the entity type TypedContext, once retrieved through a get operation
|
||||
you will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
|
||||
entity_type, entity_id = entities[0]
|
||||
entity = session.get(entity_type, entity_id)
|
||||
|
||||
# TODO Should return False if not TASK ?!!!
|
||||
# TODO Should return False if more than one entity is selected ?!!!
|
||||
if (
|
||||
len(entities) > 1 or
|
||||
entity.entity_type.lower() != 'task'
|
||||
):
|
||||
return False
|
||||
|
||||
ft_project = entity['project']
|
||||
|
||||
database = ftrack_utils.get_avalon_database()
|
||||
project_name = ft_project['full_name']
|
||||
avalon_project = database[project_name].find_one({
|
||||
"type": "project"
|
||||
})
|
||||
|
||||
if avalon_project is None:
|
||||
return False
|
||||
else:
|
||||
apps = []
|
||||
for app in avalon_project['config']['apps']:
|
||||
apps.append(app['name'])
|
||||
|
||||
if self.identifier not in apps:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _translate_event(self, session, event):
|
||||
'''Return *event* translated structure to be used with the API.'''
|
||||
|
||||
_selection = event['data'].get('selection', [])
|
||||
|
||||
_entities = list()
|
||||
for entity in _selection:
|
||||
_entities.append(
|
||||
(
|
||||
self._get_entity_type(entity), entity.get('entityId')
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
_entities,
|
||||
event
|
||||
]
|
||||
|
||||
def _get_entity_type(self, entity):
|
||||
'''Return translated entity type tht can be used with API.'''
|
||||
# Get entity type and make sure it is lower cased. Most places except
|
||||
# the component tab in the Sidebar will use lower case notation.
|
||||
entity_type = entity.get('entityType').replace('_', '').lower()
|
||||
|
||||
for schema in self.session.schemas:
|
||||
alias_for = schema.get('alias_for')
|
||||
|
||||
if (
|
||||
alias_for and isinstance(alias_for, str) and
|
||||
alias_for.lower() == entity_type
|
||||
):
|
||||
return schema['id']
|
||||
|
||||
for schema in self.session.schemas:
|
||||
if schema['id'].lower() == entity_type:
|
||||
return schema['id']
|
||||
|
||||
raise ValueError(
|
||||
'Unable to translate entity type: {0}.'.format(entity_type)
|
||||
)
|
||||
|
||||
def _launch(self, event):
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
interface = self._interface(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
if interface:
|
||||
return interface
|
||||
|
||||
response = self.launch(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
return self._handle_result(
|
||||
self.session, response, *args
|
||||
)
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
'''Callback method for the custom action.
|
||||
|
||||
return either a bool ( True if successful or False if the action failed )
|
||||
or a dictionary with they keys `message` and `success`, the message should be a
|
||||
string and will be displayed as feedback to the user, success should be a bool,
|
||||
True if successful or False if the action failed.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
|
||||
self.log.info((
|
||||
"Action - {0} ({1}) - just started"
|
||||
).format(self.label, self.identifier))
|
||||
|
||||
entity, id = entities[0]
|
||||
entity = session.get(entity, id)
|
||||
project_name = entity['project']['full_name']
|
||||
|
||||
database = ftrack_utils.get_avalon_database()
|
||||
|
||||
# Get current environments
|
||||
env_list = [
|
||||
'AVALON_PROJECT',
|
||||
'AVALON_SILO',
|
||||
'AVALON_ASSET',
|
||||
'AVALON_TASK',
|
||||
'AVALON_APP',
|
||||
'AVALON_APP_NAME'
|
||||
]
|
||||
env_origin = {}
|
||||
for env in env_list:
|
||||
env_origin[env] = os.environ.get(env, None)
|
||||
|
||||
# set environments for Avalon
|
||||
os.environ["AVALON_PROJECT"] = project_name
|
||||
os.environ["AVALON_SILO"] = entity['ancestors'][0]['name']
|
||||
os.environ["AVALON_ASSET"] = entity['parent']['name']
|
||||
os.environ["AVALON_TASK"] = entity['name']
|
||||
os.environ["AVALON_APP"] = self.identifier.split("_")[0]
|
||||
os.environ["AVALON_APP_NAME"] = self.identifier
|
||||
|
||||
anatomy = pype.Anatomy
|
||||
hierarchy = database[project_name].find_one({
|
||||
"type": 'asset',
|
||||
"name": entity['parent']['name']
|
||||
})['data']['parents']
|
||||
|
||||
if hierarchy:
|
||||
hierarchy = os.path.join(*hierarchy)
|
||||
|
||||
data = {"project": {"name": entity['project']['full_name'],
|
||||
"code": entity['project']['name']},
|
||||
"task": entity['name'],
|
||||
"asset": entity['parent']['name'],
|
||||
"hierarchy": hierarchy}
|
||||
try:
|
||||
anatomy = anatomy.format(data)
|
||||
except Exception as e:
|
||||
self.log.error(
|
||||
"{0} Error in anatomy.format: {1}".format(__name__, e)
|
||||
)
|
||||
os.environ["AVALON_WORKDIR"] = os.path.join(
|
||||
anatomy.work.root, anatomy.work.folder
|
||||
)
|
||||
|
||||
# collect all parents from the task
|
||||
parents = []
|
||||
for item in entity['link']:
|
||||
parents.append(session.get(item['type'], item['id']))
|
||||
|
||||
# collect all the 'environment' attributes from parents
|
||||
tools_attr = [os.environ["AVALON_APP"], os.environ["AVALON_APP_NAME"]]
|
||||
for parent in reversed(parents):
|
||||
# check if the attribute is empty, if not use it
|
||||
if parent['custom_attributes']['tools_env']:
|
||||
tools_attr.extend(parent['custom_attributes']['tools_env'])
|
||||
break
|
||||
|
||||
tools_env = acre.get_tools(tools_attr)
|
||||
env = acre.compute(tools_env)
|
||||
env = acre.merge(env, current_env=dict(os.environ))
|
||||
|
||||
# Get path to execute
|
||||
st_temp_path = os.environ['PYPE_STUDIO_TEMPLATES']
|
||||
os_plat = platform.system().lower()
|
||||
|
||||
# Path to folder with launchers
|
||||
path = os.path.join(st_temp_path, 'bin', os_plat)
|
||||
# Full path to executable launcher
|
||||
execfile = None
|
||||
|
||||
if sys.platform == "win32":
|
||||
|
||||
for ext in os.environ["PATHEXT"].split(os.pathsep):
|
||||
fpath = os.path.join(path.strip('"'), self.executable + ext)
|
||||
if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
|
||||
execfile = fpath
|
||||
break
|
||||
pass
|
||||
|
||||
# Run SW if was found executable
|
||||
if execfile is not None:
|
||||
lib.launch(executable=execfile, args=[], environment=env)
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'message': "We didn't found launcher for {0}"
|
||||
.format(self.label)
|
||||
}
|
||||
pass
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
execfile = os.path.join(path.strip('"'), self.executable)
|
||||
if os.path.isfile(execfile):
|
||||
try:
|
||||
fp = open(execfile)
|
||||
except PermissionError as p:
|
||||
self.log.error('Access denied on {0} - {1}'.format(
|
||||
execfile, p))
|
||||
return {
|
||||
'success': False,
|
||||
'message': "Access denied on launcher - {}".format(
|
||||
execfile)
|
||||
}
|
||||
fp.close()
|
||||
# check executable permission
|
||||
if not os.access(execfile, os.X_OK):
|
||||
self.log.error('No executable permission on {}'.format(
|
||||
execfile))
|
||||
return {
|
||||
'success': False,
|
||||
'message': "No executable permission - {}".format(
|
||||
execfile)
|
||||
}
|
||||
pass
|
||||
else:
|
||||
self.log.error('Launcher doesn\'t exist - {}'.format(
|
||||
execfile))
|
||||
return {
|
||||
'success': False,
|
||||
'message': "Launcher doesn't exist - {}".format(execfile)
|
||||
}
|
||||
pass
|
||||
# Run SW if was found executable
|
||||
if execfile is not None:
|
||||
lib.launch(
|
||||
'/usr/bin/env', args=['bash', execfile], environment=env
|
||||
)
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'message': "We didn't found launcher for {0}"
|
||||
.format(self.label)
|
||||
}
|
||||
pass
|
||||
|
||||
# RUN TIMER IN FTRACK
|
||||
username = event['source']['user']['username']
|
||||
user_query = 'User where username is "{}"'.format(username)
|
||||
user = session.query(user_query).one()
|
||||
task = session.query('Task where id is {}'.format(entity['id'])).one()
|
||||
self.log.info('Starting timer for task: ' + task['name'])
|
||||
user.start_timer(task, force=True)
|
||||
|
||||
# Change status of task to In progress
|
||||
config = ftrack_utils.get_config_data()
|
||||
|
||||
if (
|
||||
'status_on_app_launch' in config and
|
||||
'sync_to_avalon' in config and
|
||||
'statuses_name_change' in config['sync_to_avalon']
|
||||
):
|
||||
statuses = config['sync_to_avalon']['statuses_name_change']
|
||||
if entity['status']['name'].lower() in statuses:
|
||||
status_name = config['status_on_app_launch']
|
||||
|
||||
try:
|
||||
query = 'Status where name is "{}"'.format(status_name)
|
||||
status = session.query(query).one()
|
||||
task['status'] = status
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
msg = "Status '{}' in config wasn't found on Ftrack".format(status_name)
|
||||
self.log.warning(msg)
|
||||
|
||||
# Set origin avalon environments
|
||||
for key, value in env_origin.items():
|
||||
os.environ[key] = value
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': "Launching {0}".format(self.label)
|
||||
}
|
||||
|
||||
def _interface(self, *args):
|
||||
interface = self.interface(*args)
|
||||
|
||||
if interface:
|
||||
return {
|
||||
'items': interface
|
||||
}
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
'''Return a interface if applicable or None
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
'''
|
||||
return None
|
||||
|
||||
def _handle_result(self, session, result, entities, event):
|
||||
'''Validate the returned result from the action callback'''
|
||||
if isinstance(result, bool):
|
||||
result = {
|
||||
'success': result,
|
||||
'message': (
|
||||
'{0} launched successfully.'.format(
|
||||
self.label
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
elif isinstance(result, dict):
|
||||
for key in ('success', 'message'):
|
||||
if key in result:
|
||||
continue
|
||||
|
||||
raise KeyError(
|
||||
'Missing required key: {0}.'.format(key)
|
||||
)
|
||||
|
||||
else:
|
||||
self.log.error(
|
||||
'Invalid result type must be bool or dictionary!'
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class BaseAction(object):
|
||||
'''Custom Action base class
|
||||
|
||||
`label` a descriptive string identifing your action.
|
||||
|
||||
`varaint` To group actions together, give them the same
|
||||
label and specify a unique variant per action.
|
||||
|
||||
`identifier` a unique identifier for your action.
|
||||
|
||||
`description` a verbose descriptive text for you action
|
||||
|
||||
'''
|
||||
label = None
|
||||
variant = None
|
||||
identifier = None
|
||||
description = None
|
||||
icon = None
|
||||
|
||||
def __init__(self, session):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
|
||||
self.log = pype.Logger.getLogger(self.__class__.__name__)
|
||||
|
||||
if self.label is None:
|
||||
raise ValueError(
|
||||
'Action missing label.'
|
||||
)
|
||||
|
||||
elif self.identifier is None:
|
||||
raise ValueError(
|
||||
'Action missing identifier.'
|
||||
)
|
||||
|
||||
self._session = session
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
'''Return current session.'''
|
||||
return self._session
|
||||
|
||||
def reset_session(self):
|
||||
self.session.reset()
|
||||
|
||||
def register(self, priority=100):
|
||||
'''
|
||||
Registers the action, subscribing the the discover and launch topics.
|
||||
- highest priority event will show last
|
||||
'''
|
||||
self.session.event_hub.subscribe(
|
||||
'topic=ftrack.action.discover and source.user.username={0}'.format(
|
||||
self.session.api_user
|
||||
), self._discover, priority=priority
|
||||
)
|
||||
|
||||
launch_subscription = (
|
||||
'topic=ftrack.action.launch'
|
||||
' and data.actionIdentifier={0}'
|
||||
' and source.user.username={1}'
|
||||
).format(
|
||||
self.identifier,
|
||||
self.session.api_user
|
||||
)
|
||||
self.session.event_hub.subscribe(
|
||||
launch_subscription,
|
||||
self._launch
|
||||
)
|
||||
|
||||
self.log.info("Action '{}' - Registered successfully".format(
|
||||
self.__class__.__name__))
|
||||
|
||||
def _discover(self, event):
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
accepts = self.discover(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
if accepts:
|
||||
self.log.info(u'Discovering action with selection: {0}'.format(
|
||||
args[1]['data'].get('selection', [])))
|
||||
return {
|
||||
'items': [{
|
||||
'label': self.label,
|
||||
'variant': self.variant,
|
||||
'description': self.description,
|
||||
'actionIdentifier': self.identifier,
|
||||
'icon': self.icon,
|
||||
}]
|
||||
}
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
'''Return true if we can handle the selected entities.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
|
||||
return False
|
||||
|
||||
def _translate_event(self, session, event):
|
||||
'''Return *event* translated structure to be used with the API.'''
|
||||
|
||||
_selection = event['data'].get('selection', [])
|
||||
|
||||
_entities = list()
|
||||
for entity in _selection:
|
||||
_entities.append(
|
||||
(
|
||||
session.get(
|
||||
self._get_entity_type(entity),
|
||||
entity.get('entityId')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
_entities,
|
||||
event
|
||||
]
|
||||
|
||||
def _get_entity_type(self, entity):
|
||||
'''Return translated entity type tht can be used with API.'''
|
||||
# Get entity type and make sure it is lower cased. Most places except
|
||||
# the component tab in the Sidebar will use lower case notation.
|
||||
entity_type = entity.get('entityType').replace('_', '').lower()
|
||||
|
||||
for schema in self.session.schemas:
|
||||
alias_for = schema.get('alias_for')
|
||||
|
||||
if (
|
||||
alias_for and isinstance(alias_for, str) and
|
||||
alias_for.lower() == entity_type
|
||||
):
|
||||
return schema['id']
|
||||
|
||||
for schema in self.session.schemas:
|
||||
if schema['id'].lower() == entity_type:
|
||||
return schema['id']
|
||||
|
||||
raise ValueError(
|
||||
'Unable to translate entity type: {0}.'.format(entity_type)
|
||||
)
|
||||
|
||||
def _launch(self, event):
|
||||
self.reset_session()
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
interface = self._interface(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
if interface:
|
||||
return interface
|
||||
|
||||
response = self.launch(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
return self._handle_result(
|
||||
self.session, response, *args
|
||||
)
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
'''Callback method for the custom action.
|
||||
|
||||
return either a bool ( True if successful or False if the action failed )
|
||||
or a dictionary with they keys `message` and `success`, the message should be a
|
||||
string and will be displayed as feedback to the user, success should be a bool,
|
||||
True if successful or False if the action failed.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def _interface(self, *args):
|
||||
interface = self.interface(*args)
|
||||
|
||||
if interface:
|
||||
return {
|
||||
'items': interface
|
||||
}
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
'''Return a interface if applicable or None
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
'''
|
||||
return None
|
||||
|
||||
def show_message(self, event, input_message, result=False):
|
||||
"""
|
||||
Shows message to user who triggered event
|
||||
- event - just source of user id
|
||||
- input_message - message that is shown to user
|
||||
- result - changes color of message (based on ftrack settings)
|
||||
- True = Violet
|
||||
- False = Red
|
||||
"""
|
||||
if not isinstance(result, bool):
|
||||
result = False
|
||||
|
||||
try:
|
||||
message = str(input_message)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
user_id = event['source']['user']['id']
|
||||
target = (
|
||||
'applicationId=ftrack.client.web and user.id="{0}"'
|
||||
).format(user_id)
|
||||
self.session.event_hub.publish(
|
||||
ftrack_api.event.base.Event(
|
||||
topic='ftrack.action.trigger-user-interface',
|
||||
data=dict(
|
||||
type='message',
|
||||
success=result,
|
||||
message=message
|
||||
),
|
||||
target=target
|
||||
),
|
||||
on_error='ignore'
|
||||
)
|
||||
|
||||
def _handle_result(self, session, result, entities, event):
|
||||
'''Validate the returned result from the action callback'''
|
||||
if isinstance(result, bool):
|
||||
result = {
|
||||
'success': result,
|
||||
'message': (
|
||||
'{0} launched successfully.'.format(
|
||||
self.label
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
elif isinstance(result, dict):
|
||||
if 'items' in result:
|
||||
items = result['items']
|
||||
if not isinstance(items, list):
|
||||
raise ValueError('Invalid items format, must be list!')
|
||||
|
||||
else:
|
||||
for key in ('success', 'message'):
|
||||
if key in result:
|
||||
continue
|
||||
|
||||
raise KeyError(
|
||||
'Missing required key: {0}.'.format(key)
|
||||
)
|
||||
|
||||
else:
|
||||
self.log.error(
|
||||
'Invalid result type must be bool or dictionary!'
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def show_interface(self, event, items, title=''):
|
||||
"""
|
||||
Shows interface to user who triggered event
|
||||
- 'items' must be list containing Ftrack interface items
|
||||
"""
|
||||
user_id = event['source']['user']['id']
|
||||
target = (
|
||||
'applicationId=ftrack.client.web and user.id="{0}"'
|
||||
).format(user_id)
|
||||
|
||||
self.session.event_hub.publish(
|
||||
ftrack_api.event.base.Event(
|
||||
topic='ftrack.action.trigger-user-interface',
|
||||
data=dict(
|
||||
type='widget',
|
||||
items=items,
|
||||
title=title
|
||||
),
|
||||
target=target
|
||||
),
|
||||
on_error='ignore'
|
||||
)
|
||||
|
|
@ -1,228 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
import getpass
|
||||
import argparse
|
||||
import errno
|
||||
import sys
|
||||
import threading
|
||||
import ftrack
|
||||
|
||||
PLUGIN_DIRECTORY = os.path.abspath(
|
||||
os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
if PLUGIN_DIRECTORY not in sys.path:
|
||||
sys.path.append(PLUGIN_DIRECTORY)
|
||||
|
||||
import ft_utils
|
||||
|
||||
|
||||
def async(fn):
|
||||
'''Run *fn* asynchronously.'''
|
||||
def wrapper(*args, **kwargs):
|
||||
thread = threading.Thread(target=fn, args=args, kwargs=kwargs)
|
||||
thread.start()
|
||||
return wrapper
|
||||
|
||||
|
||||
class CreateFolders(ftrack.Action):
|
||||
|
||||
'''Custom action.'''
|
||||
|
||||
#: Action identifier.
|
||||
identifier = 'create.folders'
|
||||
|
||||
#: Action label.
|
||||
label = 'Create Folders'
|
||||
|
||||
#: Action Icon.
|
||||
icon = 'https://cdn1.iconfinder.com/data/icons/rcons-folder-action/32/folder_add-512.png'
|
||||
|
||||
def __init__(self):
|
||||
'''Initialise action handler.'''
|
||||
self.logger = logging.getLogger(
|
||||
__name__ + '.' + self.__class__.__name__
|
||||
)
|
||||
|
||||
def register(self):
|
||||
'''Register action.'''
|
||||
ftrack.EVENT_HUB.subscribe(
|
||||
'topic=ftrack.action.discover and source.user.username={0}'.format(
|
||||
getpass.getuser()
|
||||
),
|
||||
self.discover
|
||||
)
|
||||
|
||||
ftrack.EVENT_HUB.subscribe(
|
||||
'topic=ftrack.action.launch and source.user.username={0} '
|
||||
'and data.actionIdentifier={1}'.format(
|
||||
getpass.getuser(), self.identifier
|
||||
),
|
||||
self.launch
|
||||
)
|
||||
|
||||
@async
|
||||
def createFoldersFromEntity(self, entity):
|
||||
'''Generate folder structure from *entity*.
|
||||
|
||||
Entity is assumed to be either a project, episode, sequence or shot.
|
||||
|
||||
'''
|
||||
|
||||
root = entity.getProject().getRoot()
|
||||
|
||||
self.logger.info(root)
|
||||
|
||||
if entity.getObjectType() in (
|
||||
'Episode', 'Sequence', 'Folder', 'Shot'):
|
||||
objects = entity.getChildren(objectType='Shot', depth=None)
|
||||
objects.append(entity)
|
||||
else:
|
||||
objects = entity.getChildren(depth=None)
|
||||
|
||||
for obj in objects:
|
||||
|
||||
tasks = obj.getTasks()
|
||||
paths_collected = set([])
|
||||
if obj.getObjectType() in (
|
||||
'Episode', 'Sequence', 'Shot', 'Folder'):
|
||||
task_mask = 'shot.task'
|
||||
else:
|
||||
task_mask = 'asset.task'
|
||||
|
||||
self.logger.info(task_mask)
|
||||
|
||||
for task in tasks:
|
||||
self.logger.info(task)
|
||||
paths = ft_utils.getAllPathsYaml(task)
|
||||
self.logger.info(paths)
|
||||
for path in paths:
|
||||
if task_mask in path[1].name:
|
||||
temppath = os.path.join(
|
||||
root, path[0].lower().replace(" ", '_').replace('\'', ''))
|
||||
paths_collected.add(temppath)
|
||||
|
||||
for path in paths_collected:
|
||||
self.logger.info(path)
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError as error:
|
||||
if error.errno != errno.EEXIST:
|
||||
raise
|
||||
|
||||
def validateSelection(self, selection):
|
||||
'''Return true if the selection is valid.
|
||||
|
||||
'''
|
||||
if len(selection) == 0:
|
||||
return False
|
||||
|
||||
entity = selection[0]
|
||||
task = ftrack.Task(entity['entityId'])
|
||||
|
||||
if task.getObjectType() not in (
|
||||
'Episode', 'Sequence', 'Shot', 'Folder', 'Asset Build'):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def discover(self, event):
|
||||
|
||||
selection = event['data'].get('selection', [])
|
||||
|
||||
self.logger.info(
|
||||
u'Discovering action with selection: {0}'.format(selection))
|
||||
|
||||
if not self.validateSelection(selection):
|
||||
return
|
||||
|
||||
return {
|
||||
'items': [{
|
||||
'label': self.label,
|
||||
'actionIdentifier': self.identifier,
|
||||
'icon': self.icon,
|
||||
}]
|
||||
}
|
||||
|
||||
def launch(self, event):
|
||||
'''Callback method for custom action.'''
|
||||
selection = event['data'].get('selection', [])
|
||||
|
||||
#######################################################################
|
||||
job = ftrack.createJob(
|
||||
description="Creating Folders", status="running")
|
||||
try:
|
||||
ftrack.EVENT_HUB.publishReply(
|
||||
event,
|
||||
data={
|
||||
'success': True,
|
||||
'message': 'Folder Creation Job Started!'
|
||||
}
|
||||
)
|
||||
|
||||
for entity in selection:
|
||||
if entity['entityType'] == 'task':
|
||||
entity = ftrack.Task(entity['entityId'])
|
||||
else:
|
||||
entity = ftrack.Project(entity['entityId'])
|
||||
|
||||
self.createFoldersFromEntity(entity)
|
||||
# inform the user that the job is done
|
||||
job.setStatus('done')
|
||||
except:
|
||||
job.setStatus('failed')
|
||||
raise
|
||||
|
||||
#######################################################################
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': 'Created Folders Successfully!'
|
||||
}
|
||||
|
||||
|
||||
def register(registry, **kw):
|
||||
'''Register hooks.'''
|
||||
if registry is not ftrack.EVENT_HANDLERS:
|
||||
# Exit to avoid registering this plugin again.
|
||||
return
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
action = CreateFolders()
|
||||
action.register()
|
||||
|
||||
|
||||
def main(arguments=None):
|
||||
'''Create folders action.'''
|
||||
if arguments is None:
|
||||
arguments = []
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# Allow setting of logging level from arguments.
|
||||
loggingLevels = {}
|
||||
for level in (
|
||||
logging.NOTSET, logging.DEBUG, logging.INFO, logging.WARNING,
|
||||
logging.ERROR, logging.CRITICAL
|
||||
):
|
||||
loggingLevels[logging.getLevelName(level).lower()] = level
|
||||
|
||||
parser.add_argument(
|
||||
'-v', '--verbosity',
|
||||
help='Set the logging output verbosity.',
|
||||
choices=loggingLevels.keys(),
|
||||
default='info'
|
||||
)
|
||||
namespace = parser.parse_args(arguments)
|
||||
|
||||
'''Register action and listen for events.'''
|
||||
logging.basicConfig(level=loggingLevels[namespace.verbosity])
|
||||
|
||||
# Subscribe to action.
|
||||
ftrack.setup()
|
||||
action = CreateFolders()
|
||||
action.register()
|
||||
|
||||
ftrack.EVENT_HUB.wait()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
|
@ -4,7 +4,8 @@ import toml
|
|||
import ftrack_api
|
||||
import appdirs
|
||||
|
||||
config_path = os.path.normpath(appdirs.user_data_dir('pype-app','pype'))
|
||||
|
||||
config_path = os.path.normpath(appdirs.user_data_dir('pype-app', 'pype'))
|
||||
config_name = 'ftrack_cred.toml'
|
||||
fpath = os.path.join(config_path, config_name)
|
||||
folder = os.path.dirname(fpath)
|
||||
|
|
@ -12,6 +13,7 @@ folder = os.path.dirname(fpath)
|
|||
if not os.path.isdir(folder):
|
||||
os.makedirs(folder)
|
||||
|
||||
|
||||
def _get_credentials():
|
||||
|
||||
folder = os.path.dirname(fpath)
|
||||
|
|
@ -21,7 +23,7 @@ def _get_credentials():
|
|||
|
||||
try:
|
||||
file = open(fpath, 'r')
|
||||
except:
|
||||
except Exception:
|
||||
filecreate = open(fpath, 'w')
|
||||
filecreate.close()
|
||||
file = open(fpath, 'r')
|
||||
|
|
@ -31,25 +33,30 @@ def _get_credentials():
|
|||
|
||||
return credentials
|
||||
|
||||
|
||||
def _save_credentials(username, apiKey):
|
||||
file = open(fpath, 'w')
|
||||
|
||||
data = {
|
||||
'username':username,
|
||||
'apiKey':apiKey
|
||||
'username': username,
|
||||
'apiKey': apiKey
|
||||
}
|
||||
|
||||
credentials = toml.dumps(data)
|
||||
file.write(credentials)
|
||||
file.close()
|
||||
|
||||
|
||||
def _clear_credentials():
|
||||
file = open(fpath, 'w').close()
|
||||
_set_env(None, None)
|
||||
|
||||
|
||||
def _set_env(username, apiKey):
|
||||
os.environ['FTRACK_API_USER'] = username
|
||||
os.environ['FTRACK_API_KEY'] = apiKey
|
||||
|
||||
|
||||
def _check_credentials(username=None, apiKey=None):
|
||||
|
||||
if username and apiKey:
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
import sys
|
||||
import os
|
||||
from pype.ftrack import credentials, login_dialog as login_dialog
|
||||
from FtrackServer import FtrackServer
|
||||
from app.vendor.Qt import QtCore, QtGui, QtWidgets
|
||||
from app.vendor.Qt import QtWidgets
|
||||
from pype import api
|
||||
|
||||
log = api.Logger.getLogger(__name__, "ftrack-event-server")
|
||||
|
||||
|
||||
class EventServer:
|
||||
def __init__(self):
|
||||
self.login_widget = login_dialog.Login_Dialog_ui(self)
|
||||
self.event_server = FtrackServer('event')
|
||||
|
||||
|
||||
cred = credentials._get_credentials()
|
||||
|
||||
if 'username' in cred and 'apiKey' in cred:
|
||||
|
|
@ -27,10 +27,12 @@ class EventServer:
|
|||
self.login_widget.close()
|
||||
self.event_server.run_server()
|
||||
|
||||
|
||||
def main():
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
event = EventServer()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
if (__name__ == ('__main__')):
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import argparse
|
|||
import logging
|
||||
import ftrack_api
|
||||
import json
|
||||
from pype.ftrack import ftrack_utils
|
||||
from pype.ftrack.actions.ftrack_action_handler import BaseAction
|
||||
from pype.ftrack import BaseAction, lib
|
||||
|
||||
|
||||
class Sync_To_Avalon(BaseAction):
|
||||
|
|
@ -56,7 +55,6 @@ class Sync_To_Avalon(BaseAction):
|
|||
)
|
||||
|
||||
def register(self):
|
||||
'''Registers the action, subscribing the the discover and launch topics.'''
|
||||
self.session.event_hub.subscribe(
|
||||
'topic=ftrack.action.discover',
|
||||
self._discover
|
||||
|
|
@ -68,10 +66,6 @@ class Sync_To_Avalon(BaseAction):
|
|||
),
|
||||
self._launch
|
||||
)
|
||||
msg = (
|
||||
"Action '{}' - Registered successfully"
|
||||
).format(self.__class__.__name__)
|
||||
self.log.info(msg)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
|
|
@ -130,7 +124,7 @@ class Sync_To_Avalon(BaseAction):
|
|||
duplicates = []
|
||||
|
||||
for e in self.importable:
|
||||
ftrack_utils.avalon_check_name(e)
|
||||
lib.avalon_check_name(e)
|
||||
if e['name'] in all_names:
|
||||
duplicates.append("'{}'".format(e['name']))
|
||||
else:
|
||||
|
|
@ -144,12 +138,12 @@ class Sync_To_Avalon(BaseAction):
|
|||
# ----- PROJECT ------
|
||||
# store Ftrack project- self.importable[0] must be project entity!!
|
||||
ft_project = self.importable[0]
|
||||
avalon_project = ftrack_utils.get_avalon_project(ft_project)
|
||||
custom_attributes = ftrack_utils.get_avalon_attr(session)
|
||||
avalon_project = lib.get_avalon_project(ft_project)
|
||||
custom_attributes = lib.get_avalon_attr(session)
|
||||
|
||||
# Import all entities to Avalon DB
|
||||
for entity in self.importable:
|
||||
result = ftrack_utils.import_to_avalon(
|
||||
result = lib.import_to_avalon(
|
||||
session=session,
|
||||
entity=entity,
|
||||
ft_project=ft_project,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import ftrack_api
|
||||
from ftrack_event_handler import BaseEvent
|
||||
from pype.ftrack import BaseEvent
|
||||
import operator
|
||||
|
||||
|
||||
|
|
@ -64,14 +64,17 @@ class NextTaskUpdate(BaseEvent):
|
|||
|
||||
# Setting next task status
|
||||
try:
|
||||
status_to_set = session.query(
|
||||
'Status where name is "{}"'.format('Ready')).one()
|
||||
query = 'Status where name is "{}"'.format('Ready')
|
||||
status_to_set = session.query(query).one()
|
||||
next_task['status'] = status_to_set
|
||||
except Exception as e:
|
||||
self.log.warning('!!! [ {} ] status couldnt be set: [ {} ]'.format(
|
||||
path, e))
|
||||
self.log.warning((
|
||||
'!!! [ {} ] status couldnt be set: [ {} ]'
|
||||
).format(path, e))
|
||||
else:
|
||||
self.log.info('>>> [ {} ] updated to [ Ready ]'.format(path))
|
||||
self.log.info((
|
||||
'>>> [ {} ] updated to [ Ready ]'
|
||||
).format(path))
|
||||
|
||||
session.commit()
|
||||
|
||||
40
pype/ftrack/events/event_radio_buttons.py
Normal file
40
pype/ftrack/events/event_radio_buttons.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import ftrack_api
|
||||
from pype.ftrack import BaseEvent
|
||||
|
||||
|
||||
class Radio_buttons(BaseEvent):
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
'''Provides a readio button behaviour to any bolean attribute in
|
||||
radio_button group.'''
|
||||
|
||||
# start of event procedure ----------------------------------
|
||||
for entity in event['data'].get('entities', []):
|
||||
|
||||
if entity['entityType'] == 'assetversion':
|
||||
|
||||
query = 'CustomAttributeGroup where name is "radio_button"'
|
||||
group = session.query(query).one()
|
||||
radio_buttons = []
|
||||
for g in group['custom_attribute_configurations']:
|
||||
radio_buttons.append(g['key'])
|
||||
|
||||
for key in entity['keys']:
|
||||
if (key in radio_buttons and entity['changes'] is not None):
|
||||
if entity['changes'][key]['new'] == '1':
|
||||
version = session.get('AssetVersion',
|
||||
entity['entityId'])
|
||||
asset = session.get('Asset', entity['parentId'])
|
||||
for v in asset['versions']:
|
||||
if version is not v:
|
||||
v['custom_attributes'][key] = 0
|
||||
|
||||
session.commit()
|
||||
|
||||
|
||||
def register(session):
|
||||
'''Register plugin. Called when used as an plugin.'''
|
||||
if not isinstance(session, ftrack_api.session.Session):
|
||||
return
|
||||
|
||||
Radio_buttons(session).register()
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
import os
|
||||
import ftrack_api
|
||||
from pype.ftrack import ftrack_utils
|
||||
from ftrack_event_handler import BaseEvent
|
||||
from pype.ftrack import BaseEvent, lib
|
||||
|
||||
|
||||
class Sync_to_Avalon(BaseEvent):
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
|
||||
ca_mongoid = ftrack_utils.get_ca_mongoid()
|
||||
ca_mongoid = lib.get_ca_mongoid()
|
||||
# If mongo_id textfield has changed: RETURN!
|
||||
# - infinite loop
|
||||
for ent in event['data']['entities']:
|
||||
|
|
@ -47,9 +45,9 @@ class Sync_to_Avalon(BaseEvent):
|
|||
# get avalon project if possible
|
||||
import_entities = []
|
||||
|
||||
custom_attributes = ftrack_utils.get_avalon_attr(session)
|
||||
custom_attributes = lib.get_avalon_attr(session)
|
||||
|
||||
avalon_project = ftrack_utils.get_avalon_project(ft_project)
|
||||
avalon_project = lib.get_avalon_project(ft_project)
|
||||
if avalon_project is None:
|
||||
import_entities.append(ft_project)
|
||||
|
||||
|
|
@ -78,7 +76,7 @@ class Sync_to_Avalon(BaseEvent):
|
|||
|
||||
try:
|
||||
for entity in import_entities:
|
||||
result = ftrack_utils.import_to_avalon(
|
||||
result = lib.import_to_avalon(
|
||||
session=session,
|
||||
entity=entity,
|
||||
ft_project=ft_project,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,22 @@ import os
|
|||
import sys
|
||||
import re
|
||||
import ftrack_api
|
||||
from ftrack_event_handler import BaseEvent
|
||||
from pype.ftrack import BaseEvent
|
||||
from app import api
|
||||
|
||||
|
||||
ignore_me = True
|
||||
|
||||
|
||||
class Test_Event(BaseEvent):
|
||||
|
||||
priority = 10000
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
|
||||
'''just a testing event'''
|
||||
|
||||
# self.log.info(event)
|
||||
self.log.info(event)
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import ftrack_api
|
||||
from ftrack_event_handler import BaseEvent
|
||||
from pype.ftrack import BaseEvent
|
||||
|
||||
|
||||
class ThumbnailEvents(BaseEvent):
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import ftrack_api
|
||||
from ftrack_event_handler import BaseEvent
|
||||
from pype.ftrack import BaseEvent
|
||||
|
||||
|
||||
class VersionToTaskStatus(BaseEvent):
|
||||
|
|
@ -15,8 +15,9 @@ class VersionToTaskStatus(BaseEvent):
|
|||
'statusid' in entity['keys']):
|
||||
|
||||
version = session.get('AssetVersion', entity['entityId'])
|
||||
version_status = session.get('Status',
|
||||
entity['changes']['statusid']['new'])
|
||||
version_status = session.get(
|
||||
'Status', entity['changes']['statusid']['new']
|
||||
)
|
||||
task_status = version_status
|
||||
task = version['task']
|
||||
self.log.info('>>> version status: [ {} ]'.format(
|
||||
|
|
@ -34,8 +35,8 @@ class VersionToTaskStatus(BaseEvent):
|
|||
'>>> status to set: [ {} ]'.format(status_to_set))
|
||||
|
||||
if status_to_set is not None:
|
||||
task_status = session.query(
|
||||
'Status where name is "{}"'.format(status_to_set)).one()
|
||||
query = 'Status where name is "{}"'.format(status_to_set)
|
||||
task_status = session.query(query).one()
|
||||
|
||||
# Proceed if the task status was set
|
||||
if task_status:
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
# :coding: utf-8
|
||||
# :copyright: Copyright (c) 2017 ftrack
|
||||
import ftrack_api
|
||||
from app.api import Logger
|
||||
|
||||
|
||||
class BaseEvent(object):
|
||||
'''Custom Event base class
|
||||
|
||||
BaseEvent is based on ftrack.update event
|
||||
- get entities from event
|
||||
|
||||
If want to use different event base
|
||||
- override register and *optional _translate_event method
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, session):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
|
||||
self.log = Logger.getLogger(self.__class__.__name__)
|
||||
|
||||
self._session = session
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
'''Return current session.'''
|
||||
return self._session
|
||||
|
||||
def register(self):
|
||||
'''Registers the event, subscribing the the discover and launch topics.'''
|
||||
self.session.event_hub.subscribe('topic=ftrack.update', self._launch)
|
||||
|
||||
self.log.info("Event '{}' - Registered successfully".format(self.__class__.__name__))
|
||||
|
||||
def _translate_event(self, session, event):
|
||||
'''Return *event* translated structure to be used with the API.'''
|
||||
_selection = event['data'].get('entities', [])
|
||||
|
||||
_entities = list()
|
||||
for entity in _selection:
|
||||
if entity['entityType'] in ['socialfeed']:
|
||||
continue
|
||||
_entities.append(
|
||||
(
|
||||
session.get(self._get_entity_type(entity), entity.get('entityId'))
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
_entities,
|
||||
event
|
||||
]
|
||||
|
||||
def _get_entity_type(self, entity):
|
||||
'''Return translated entity type tht can be used with API.'''
|
||||
# Get entity type and make sure it is lower cased. Most places except
|
||||
# the component tab in the Sidebar will use lower case notation.
|
||||
entity_type = entity.get('entityType').replace('_', '').lower()
|
||||
|
||||
for schema in self.session.schemas:
|
||||
alias_for = schema.get('alias_for')
|
||||
|
||||
if (
|
||||
alias_for and isinstance(alias_for, str) and
|
||||
alias_for.lower() == entity_type
|
||||
):
|
||||
return schema['id']
|
||||
|
||||
for schema in self.session.schemas:
|
||||
if schema['id'].lower() == entity_type:
|
||||
return schema['id']
|
||||
|
||||
raise ValueError(
|
||||
'Unable to translate entity type: {0}.'.format(entity_type)
|
||||
)
|
||||
|
||||
def _launch(self, event):
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
self.launch(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
'''Callback method for the custom action.
|
||||
|
||||
return either a bool ( True if successful or False if the action failed )
|
||||
or a dictionary with they keys `message` and `success`, the message should be a
|
||||
string and will be displayed as feedback to the user, success should be a bool,
|
||||
True if successful or False if the action failed.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def show_message(self, event, input_message, result=False):
|
||||
"""
|
||||
Shows message to user who triggered event
|
||||
- event - just source of user id
|
||||
- input_message - message that is shown to user
|
||||
- result - changes color of message (based on ftrack settings)
|
||||
- True = Violet
|
||||
- False = Red
|
||||
"""
|
||||
if not isinstance(result, bool):
|
||||
result = False
|
||||
|
||||
try:
|
||||
message = str(input_message)
|
||||
except:
|
||||
return
|
||||
|
||||
user_id = event['source']['user']['id']
|
||||
target = 'applicationId=ftrack.client.web and user.id="{0}"'.format(user_id)
|
||||
|
||||
self.session.event_hub.publish(
|
||||
ftrack_api.event.base.Event(
|
||||
topic='ftrack.action.trigger-user-interface',
|
||||
data=dict(
|
||||
type='message',
|
||||
success=result,
|
||||
message=message
|
||||
),
|
||||
target=target
|
||||
),
|
||||
on_error='ignore'
|
||||
)
|
||||
|
||||
def show_interface(self, event, items, title=''):
|
||||
"""
|
||||
Shows interface to user who triggered event
|
||||
- 'items' must be list containing Ftrack interface items
|
||||
"""
|
||||
user_id = event['source']['user']['id']
|
||||
target = 'applicationId=ftrack.client.web and user.id="{0}"'.format(user_id)
|
||||
|
||||
self.session.event_hub.publish(
|
||||
ftrack_api.event.base.Event(
|
||||
topic='ftrack.action.trigger-user-interface',
|
||||
data=dict(
|
||||
type='widget',
|
||||
items=items,
|
||||
title=title
|
||||
),
|
||||
target=target
|
||||
),
|
||||
on_error='ignore'
|
||||
)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# import ftrack_api as local session
|
||||
import ftrack_api
|
||||
from utils import print_entity_head
|
||||
#
|
||||
session = ftrack_api.Session()
|
||||
|
||||
# ----------------------------------
|
||||
|
||||
|
||||
def file_version_statuses(event):
|
||||
'''Set new version status to data if version matches given types'''
|
||||
|
||||
# start of event procedure ----------------------------------
|
||||
for entity in event['data'].get('entities', []):
|
||||
|
||||
# Filter to new assetversions
|
||||
if (entity['entityType'] == 'assetversion'
|
||||
and entity['action'] == 'add'):
|
||||
|
||||
print "\n\nevent script: {}".format(__file__)
|
||||
print_entity_head.print_entity_head(entity, session)
|
||||
|
||||
version = session.get('AssetVersion', entity['entityId'])
|
||||
asset_type = version['asset']['type']['name']
|
||||
file_status = session.query(
|
||||
'Status where name is "{}"'.format('data')).one()
|
||||
|
||||
# Setting task status
|
||||
try:
|
||||
if asset_type.lower() in ['cam', 'cache', 'rig', 'scene']:
|
||||
version['status'] = file_status
|
||||
except Exception as e:
|
||||
print '!!! status couldnt be set [ {} ]'.format(e)
|
||||
else:
|
||||
print '>>> updated to [ {} ]'.format(file_status['name'])
|
||||
|
||||
session.commit()
|
||||
# end of event procedure ----------------------------------
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
# import ftrack_api as local session
|
||||
import ftrack_api
|
||||
from utils import print_entity_head
|
||||
#
|
||||
session = ftrack_api.Session()
|
||||
|
||||
# ----------------------------------
|
||||
|
||||
|
||||
def radio_buttons(event):
|
||||
'''Provides a readio button behaviour to any bolean attribute in
|
||||
radio_button group.'''
|
||||
|
||||
# start of event procedure ----------------------------------
|
||||
for entity in event['data'].get('entities', []):
|
||||
|
||||
if entity['entityType'] == 'assetversion':
|
||||
|
||||
print "\n\nevent script: {}".format(__file__)
|
||||
print_entity_head.print_entity_head(entity, session)
|
||||
|
||||
group = session.query(
|
||||
'CustomAttributeGroup where name is "radio_button"').one()
|
||||
radio_buttons = []
|
||||
for g in group['custom_attribute_configurations']:
|
||||
radio_buttons.append(g['key'])
|
||||
|
||||
for key in entity['keys']:
|
||||
if (key in radio_buttons and entity['changes'] is not None):
|
||||
if entity['changes'][key]['new'] == '1':
|
||||
version = session.get('AssetVersion',
|
||||
entity['entityId'])
|
||||
asset = session.get('Asset', entity['parentId'])
|
||||
for v in asset['versions']:
|
||||
if version is not v:
|
||||
v['custom_attributes'][key] = 0
|
||||
|
||||
session.commit()
|
||||
# end of event procedure ----------------------------------
|
||||
|
|
@ -492,7 +492,10 @@ class StopTimer(QtWidgets.QWidget):
|
|||
self.main_context = True
|
||||
self.parent = parent
|
||||
self.setWindowIcon(self.parent.parent.icon)
|
||||
self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint)
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint |
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
from .ftrack_utils import *
|
||||
from .avalon_sync import *
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
from pype import lib
|
||||
import avalon
|
||||
import avalon.api
|
||||
from avalon.vendor import toml, jsonschema
|
||||
from app.api import Logger
|
||||
|
||||
log = Logger.getLogger(__name__)
|
||||
|
||||
|
||||
def get_config_data():
|
||||
templates = os.environ['PYPE_STUDIO_TEMPLATES']
|
||||
path_items = [templates, 'presets', 'ftrack', 'ftrack_config.json']
|
||||
filepath = os.path.sep.join(path_items)
|
||||
data = dict()
|
||||
try:
|
||||
with open(filepath) as data_file:
|
||||
data = json.load(data_file)
|
||||
|
||||
except Exception as e:
|
||||
msg = (
|
||||
'Loading "Ftrack Config file" Failed.'
|
||||
' Please check log for more information.'
|
||||
' Times are set to default.'
|
||||
)
|
||||
log.warning("{} - {}".format(msg, str(e)))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def avalon_check_name(entity, inSchema=None):
|
||||
ValidationError = jsonschema.ValidationError
|
||||
alright = True
|
||||
name = entity['name']
|
||||
if " " in name:
|
||||
alright = False
|
||||
|
||||
data = {}
|
||||
data['data'] = {}
|
||||
data['type'] = 'asset'
|
||||
schema = "avalon-core:asset-2.0"
|
||||
# TODO have project any REGEX check?
|
||||
if entity.entity_type in ['Project']:
|
||||
# data['type'] = 'project'
|
||||
name = entity['full_name']
|
||||
# schema = get_avalon_project_template_schema()
|
||||
# elif entity.entity_type in ['AssetBuild','Library']:
|
||||
# data['silo'] = 'Assets'
|
||||
# else:
|
||||
# data['silo'] = 'Film'
|
||||
data['silo'] = 'Film'
|
||||
|
||||
if inSchema is not None:
|
||||
schema = inSchema
|
||||
data['schema'] = schema
|
||||
data['name'] = name
|
||||
try:
|
||||
avalon.schema.validate(data)
|
||||
except ValidationError:
|
||||
alright = False
|
||||
|
||||
if alright is False:
|
||||
msg = '"{}" includes unsupported symbols like "dash" or "space"'
|
||||
raise ValueError(msg.format(name))
|
||||
|
||||
|
||||
def get_apps(entity):
|
||||
""" Get apps from project
|
||||
Requirements:
|
||||
'Entity' MUST be object of ftrack entity with entity_type 'Project'
|
||||
Checking if app from ftrack is available in Templates/bin/{app_name}.toml
|
||||
|
||||
Returns:
|
||||
Array with dictionaries with app Name and Label
|
||||
"""
|
||||
apps = []
|
||||
for app in entity['custom_attributes']['applications']:
|
||||
try:
|
||||
app_config = {}
|
||||
app_config['name'] = app
|
||||
app_config['label'] = toml.load(avalon.lib.which_app(app))['label']
|
||||
|
||||
apps.append(app_config)
|
||||
|
||||
except Exception as e:
|
||||
log.warning('Error with application {0} - {1}'.format(app, e))
|
||||
return apps
|
||||
|
||||
|
||||
def get_config(entity):
|
||||
config = {}
|
||||
config['schema'] = lib.get_avalon_project_config_schema()
|
||||
config['tasks'] = [{'name': ''}]
|
||||
config['apps'] = get_apps(entity)
|
||||
config['template'] = lib.get_avalon_project_template()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_context(entity):
|
||||
parents = []
|
||||
item = entity
|
||||
while True:
|
||||
item = item['parent']
|
||||
if not item:
|
||||
break
|
||||
parents.append(item)
|
||||
|
||||
ctx = collections.OrderedDict()
|
||||
folder_counter = 0
|
||||
|
||||
entityDic = {
|
||||
'name': entity['name'],
|
||||
'id': entity['id'],
|
||||
}
|
||||
try:
|
||||
entityDic['type'] = entity['type']['name']
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ctx[entity['object_type']['name']] = entityDic
|
||||
|
||||
# add all parents to the context
|
||||
for parent in parents:
|
||||
tempdic = {}
|
||||
if not parent.get('project_schema'):
|
||||
tempdic = {
|
||||
'name': parent['name'],
|
||||
'id': parent['id'],
|
||||
}
|
||||
object_type = parent['object_type']['name']
|
||||
|
||||
if object_type == 'Folder':
|
||||
object_type = object_type + str(folder_counter)
|
||||
folder_counter += 1
|
||||
|
||||
ctx[object_type] = tempdic
|
||||
|
||||
# add project to the context
|
||||
project = entity['project']
|
||||
ctx['Project'] = {
|
||||
'name': project['full_name'],
|
||||
'code': project['name'],
|
||||
'id': project['id'],
|
||||
'root': project['root']
|
||||
}
|
||||
|
||||
return ctx
|
||||
|
||||
|
||||
def get_status_by_name(name):
|
||||
statuses = ftrack.getTaskStatuses()
|
||||
|
||||
result = None
|
||||
for s in statuses:
|
||||
if s.get('name').lower() == name.lower():
|
||||
result = s
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def sort_types(types):
|
||||
data = {}
|
||||
for t in types:
|
||||
data[t] = t.get('sort')
|
||||
|
||||
data = sorted(data.items(), key=operator.itemgetter(1))
|
||||
results = []
|
||||
for item in data:
|
||||
results.append(item[0])
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def get_next_task(task):
|
||||
shot = task.getParent()
|
||||
tasks = shot.getTasks()
|
||||
|
||||
types_sorted = sort_types(ftrack.getTaskTypes())
|
||||
|
||||
next_types = None
|
||||
for t in types_sorted:
|
||||
if t.get('typeid') == task.get('typeid'):
|
||||
try:
|
||||
next_types = types_sorted[(types_sorted.index(t) + 1):]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for nt in next_types:
|
||||
for t in tasks:
|
||||
if nt.get('typeid') == t.get('typeid'):
|
||||
return t
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_latest_version(versions):
|
||||
latestVersion = None
|
||||
if len(versions) > 0:
|
||||
versionNumber = 0
|
||||
for item in versions:
|
||||
if item.get('version') > versionNumber:
|
||||
versionNumber = item.getVersion()
|
||||
latestVersion = item
|
||||
return latestVersion
|
||||
|
||||
|
||||
def get_thumbnail_recursive(task):
|
||||
if task.get('thumbid'):
|
||||
thumbid = task.get('thumbid')
|
||||
return ftrack.Attachment(id=thumbid)
|
||||
if not task.get('thumbid'):
|
||||
parent = ftrack.Task(id=task.get('parent_id'))
|
||||
return get_thumbnail_recursive(parent)
|
||||
5
pype/ftrack/lib/__init__.py
Normal file
5
pype/ftrack/lib/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .avalon_sync import *
|
||||
from .ftrack_app_handler import *
|
||||
from .ftrack_event_handler import *
|
||||
from .ftrack_action_handler import *
|
||||
from .ftrack_base_handler import *
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
import os
|
||||
import re
|
||||
from pype import lib
|
||||
import json
|
||||
from pype import lib as pypelib
|
||||
from pype.lib import get_avalon_database
|
||||
from avalon import schema
|
||||
from bson.objectid import ObjectId
|
||||
from pype.ftrack.ftrack_utils import ftrack_utils
|
||||
from avalon.vendor import jsonschema
|
||||
import avalon
|
||||
import avalon.api
|
||||
from avalon import schema
|
||||
from avalon.vendor import toml, jsonschema
|
||||
from app.api import Logger
|
||||
|
||||
ValidationError = jsonschema.ValidationError
|
||||
|
||||
log = Logger.getLogger(__name__)
|
||||
|
|
@ -38,7 +41,7 @@ def import_to_avalon(
|
|||
|
||||
# Validate if entity name match REGEX in schema
|
||||
try:
|
||||
ftrack_utils.avalon_check_name(entity)
|
||||
avalon_check_name(entity)
|
||||
except ValidationError:
|
||||
msg = '"{}" includes unsupported symbols like "dash" or "space"'
|
||||
errors.append({'Unsupported character': msg})
|
||||
|
|
@ -50,7 +53,7 @@ def import_to_avalon(
|
|||
if entity_type in ['Project']:
|
||||
type = 'project'
|
||||
|
||||
config = ftrack_utils.get_config(entity)
|
||||
config = get_project_config(entity)
|
||||
schema.validate(config)
|
||||
|
||||
av_project_code = None
|
||||
|
|
@ -59,7 +62,7 @@ def import_to_avalon(
|
|||
ft_project_code = ft_project['name']
|
||||
|
||||
if av_project is None:
|
||||
project_schema = lib.get_avalon_project_template_schema()
|
||||
project_schema = pypelib.get_avalon_project_template_schema()
|
||||
item = {
|
||||
'schema': project_schema,
|
||||
'type': type,
|
||||
|
|
@ -185,7 +188,7 @@ def import_to_avalon(
|
|||
{'type': 'asset', 'name': name}
|
||||
)
|
||||
if avalon_asset is None:
|
||||
asset_schema = lib.get_avalon_asset_template_schema()
|
||||
asset_schema = pypelib.get_avalon_asset_template_schema()
|
||||
item = {
|
||||
'schema': asset_schema,
|
||||
'name': name,
|
||||
|
|
@ -304,7 +307,7 @@ def changeability_check_childs(entity):
|
|||
childs = entity['children']
|
||||
for child in childs:
|
||||
if child.entity_type.lower() == 'task':
|
||||
config = ftrack_utils.get_config_data()
|
||||
config = get_config_data()
|
||||
if 'sync_to_avalon' in config:
|
||||
config = config['sync_to_avalon']
|
||||
if 'statuses_name_change' in config:
|
||||
|
|
@ -430,3 +433,87 @@ def get_avalon_project(ft_project):
|
|||
})
|
||||
|
||||
return avalon_project
|
||||
|
||||
|
||||
def get_project_config(entity):
|
||||
config = {}
|
||||
config['schema'] = pypelib.get_avalon_project_config_schema()
|
||||
config['tasks'] = [{'name': ''}]
|
||||
config['apps'] = get_project_apps(entity)
|
||||
config['template'] = pypelib.get_avalon_project_template()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def get_project_apps(entity):
|
||||
""" Get apps from project
|
||||
Requirements:
|
||||
'Entity' MUST be object of ftrack entity with entity_type 'Project'
|
||||
Checking if app from ftrack is available in Templates/bin/{app_name}.toml
|
||||
|
||||
Returns:
|
||||
Array with dictionaries with app Name and Label
|
||||
"""
|
||||
apps = []
|
||||
for app in entity['custom_attributes']['applications']:
|
||||
try:
|
||||
app_config = {}
|
||||
app_config['name'] = app
|
||||
app_config['label'] = toml.load(avalon.lib.which_app(app))['label']
|
||||
|
||||
apps.append(app_config)
|
||||
|
||||
except Exception as e:
|
||||
log.warning('Error with application {0} - {1}'.format(app, e))
|
||||
return apps
|
||||
|
||||
|
||||
def avalon_check_name(entity, inSchema=None):
|
||||
ValidationError = jsonschema.ValidationError
|
||||
alright = True
|
||||
name = entity['name']
|
||||
if " " in name:
|
||||
alright = False
|
||||
|
||||
data = {}
|
||||
data['data'] = {}
|
||||
data['type'] = 'asset'
|
||||
schema = "avalon-core:asset-2.0"
|
||||
# TODO have project any REGEX check?
|
||||
if entity.entity_type in ['Project']:
|
||||
# data['type'] = 'project'
|
||||
name = entity['full_name']
|
||||
# schema = get_avalon_project_template_schema()
|
||||
|
||||
data['silo'] = 'Film'
|
||||
|
||||
if inSchema is not None:
|
||||
schema = inSchema
|
||||
data['schema'] = schema
|
||||
data['name'] = name
|
||||
try:
|
||||
avalon.schema.validate(data)
|
||||
except ValidationError:
|
||||
alright = False
|
||||
|
||||
if alright is False:
|
||||
msg = '"{}" includes unsupported symbols like "dash" or "space"'
|
||||
raise ValueError(msg.format(name))
|
||||
|
||||
|
||||
def get_config_data():
|
||||
path_items = [pypelib.get_presets_path(), 'ftrack', 'ftrack_config.json']
|
||||
filepath = os.path.sep.join(path_items)
|
||||
data = dict()
|
||||
try:
|
||||
with open(filepath) as data_file:
|
||||
data = json.load(data_file)
|
||||
|
||||
except Exception as e:
|
||||
msg = (
|
||||
'Loading "Ftrack Config file" Failed.'
|
||||
' Please check log for more information.'
|
||||
)
|
||||
log.warning("{} - {}".format(msg, str(e)))
|
||||
|
||||
return data
|
||||
117
pype/ftrack/lib/ftrack_action_handler.py
Normal file
117
pype/ftrack/lib/ftrack_action_handler.py
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
from .ftrack_base_handler import BaseHandler
|
||||
|
||||
|
||||
class BaseAction(BaseHandler):
|
||||
'''Custom Action base class
|
||||
|
||||
`label` a descriptive string identifing your action.
|
||||
|
||||
`varaint` To group actions together, give them the same
|
||||
label and specify a unique variant per action.
|
||||
|
||||
`identifier` a unique identifier for your action.
|
||||
|
||||
`description` a verbose descriptive text for you action
|
||||
|
||||
'''
|
||||
label = None
|
||||
variant = None
|
||||
identifier = None
|
||||
description = None
|
||||
icon = None
|
||||
type = 'Action'
|
||||
|
||||
def __init__(self, session):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
super().__init__(session)
|
||||
|
||||
if self.label is None:
|
||||
raise ValueError(
|
||||
'Action missing label.'
|
||||
)
|
||||
|
||||
elif self.identifier is None:
|
||||
raise ValueError(
|
||||
'Action missing identifier.'
|
||||
)
|
||||
|
||||
def register(self):
|
||||
'''
|
||||
Registers the action, subscribing the the discover and launch topics.
|
||||
- highest priority event will show last
|
||||
'''
|
||||
self.session.event_hub.subscribe(
|
||||
'topic=ftrack.action.discover and source.user.username={0}'.format(
|
||||
self.session.api_user
|
||||
),
|
||||
self._discover,
|
||||
priority=self.priority
|
||||
)
|
||||
|
||||
launch_subscription = (
|
||||
'topic=ftrack.action.launch'
|
||||
' and data.actionIdentifier={0}'
|
||||
' and source.user.username={1}'
|
||||
).format(
|
||||
self.identifier,
|
||||
self.session.api_user
|
||||
)
|
||||
self.session.event_hub.subscribe(
|
||||
launch_subscription,
|
||||
self._launch
|
||||
)
|
||||
|
||||
def _launch(self, event):
|
||||
self.reset_session()
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
interface = self._interface(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
if interface:
|
||||
return interface
|
||||
|
||||
response = self.launch(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
return self._handle_result(
|
||||
self.session, response, *args
|
||||
)
|
||||
|
||||
def _handle_result(self, session, result, entities, event):
|
||||
'''Validate the returned result from the action callback'''
|
||||
if isinstance(result, bool):
|
||||
result = {
|
||||
'success': result,
|
||||
'message': (
|
||||
'{0} launched successfully.'.format(
|
||||
self.label
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
elif isinstance(result, dict):
|
||||
if 'items' in result:
|
||||
items = result['items']
|
||||
if not isinstance(items, list):
|
||||
raise ValueError('Invalid items format, must be list!')
|
||||
|
||||
else:
|
||||
for key in ('success', 'message'):
|
||||
if key in result:
|
||||
continue
|
||||
|
||||
raise KeyError(
|
||||
'Missing required key: {0}.'.format(key)
|
||||
)
|
||||
|
||||
else:
|
||||
self.log.error(
|
||||
'Invalid result type must be bool or dictionary!'
|
||||
)
|
||||
|
||||
return result
|
||||
335
pype/ftrack/lib/ftrack_app_handler.py
Normal file
335
pype/ftrack/lib/ftrack_app_handler.py
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
# :coding: utf-8
|
||||
# :copyright: Copyright (c) 2017 ftrack
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
from avalon import lib as avalonlib
|
||||
import acre
|
||||
from pype import api as pype
|
||||
from pype import lib as pypelib
|
||||
from .avalon_sync import get_config_data
|
||||
from .ftrack_base_handler import BaseHandler
|
||||
|
||||
|
||||
class AppAction(BaseHandler):
|
||||
'''Custom Action base class
|
||||
|
||||
<label> - a descriptive string identifing your action.
|
||||
<varaint> - To group actions together, give them the same
|
||||
label and specify a unique variant per action.
|
||||
<identifier> - a unique identifier for app.
|
||||
<description> - a verbose descriptive text for you action
|
||||
<icon> - icon in ftrack
|
||||
'''
|
||||
|
||||
type = 'Application'
|
||||
|
||||
def __init__(
|
||||
self, session, label, name, executable,
|
||||
variant=None, icon=None, description=None
|
||||
):
|
||||
super().__init__(session)
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
|
||||
if label is None:
|
||||
raise ValueError('Action missing label.')
|
||||
elif name is None:
|
||||
raise ValueError('Action missing identifier.')
|
||||
elif executable is None:
|
||||
raise ValueError('Action missing executable.')
|
||||
|
||||
self.label = label
|
||||
self.identifier = name
|
||||
self.executable = executable
|
||||
self.variant = variant
|
||||
self.icon = icon
|
||||
self.description = description
|
||||
|
||||
def register(self):
|
||||
'''Registers the action, subscribing the discover and launch topics.'''
|
||||
|
||||
discovery_subscription = (
|
||||
'topic=ftrack.action.discover and source.user.username={0}'
|
||||
).format(self.session.api_user)
|
||||
|
||||
self.session.event_hub.subscribe(
|
||||
discovery_subscription,
|
||||
self._discover,
|
||||
priority=self.priority
|
||||
)
|
||||
|
||||
launch_subscription = (
|
||||
'topic=ftrack.action.launch'
|
||||
' and data.actionIdentifier={0}'
|
||||
' and source.user.username={1}'
|
||||
).format(
|
||||
self.identifier,
|
||||
self.session.api_user
|
||||
)
|
||||
self.session.event_hub.subscribe(
|
||||
launch_subscription,
|
||||
self._launch
|
||||
)
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
'''Return true if we can handle the selected entities.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and
|
||||
the entity id. If the entity is a hierarchical you will always get
|
||||
the entity type TypedContext, once retrieved through a get operation
|
||||
you will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
|
||||
entity = entities[0]
|
||||
|
||||
# TODO Should return False if not TASK ?!!!
|
||||
# TODO Should return False if more than one entity is selected ?!!!
|
||||
if (
|
||||
len(entities) > 1 or
|
||||
entity.entity_type.lower() != 'task'
|
||||
):
|
||||
return False
|
||||
|
||||
ft_project = entity['project']
|
||||
|
||||
database = pypelib.get_avalon_database()
|
||||
project_name = ft_project['full_name']
|
||||
avalon_project = database[project_name].find_one({
|
||||
"type": "project"
|
||||
})
|
||||
|
||||
if avalon_project is None:
|
||||
return False
|
||||
else:
|
||||
apps = []
|
||||
for app in avalon_project['config']['apps']:
|
||||
apps.append(app['name'])
|
||||
|
||||
if self.identifier not in apps:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _launch(self, event):
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
response = self.launch(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
return self._handle_result(
|
||||
self.session, response, *args
|
||||
)
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
'''Callback method for the custom action.
|
||||
|
||||
return either a bool ( True if successful or False if the action failed )
|
||||
or a dictionary with they keys `message` and `success`, the message should be a
|
||||
string and will be displayed as feedback to the user, success should be a bool,
|
||||
True if successful or False if the action failed.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
|
||||
self.log.info((
|
||||
"Action - {0} ({1}) - just started"
|
||||
).format(self.label, self.identifier))
|
||||
|
||||
entity = entities[0]
|
||||
project_name = entity['project']['full_name']
|
||||
|
||||
database = pypelib.get_avalon_database()
|
||||
|
||||
# Get current environments
|
||||
env_list = [
|
||||
'AVALON_PROJECT',
|
||||
'AVALON_SILO',
|
||||
'AVALON_ASSET',
|
||||
'AVALON_TASK',
|
||||
'AVALON_APP',
|
||||
'AVALON_APP_NAME'
|
||||
]
|
||||
env_origin = {}
|
||||
for env in env_list:
|
||||
env_origin[env] = os.environ.get(env, None)
|
||||
|
||||
# set environments for Avalon
|
||||
os.environ["AVALON_PROJECT"] = project_name
|
||||
os.environ["AVALON_SILO"] = entity['ancestors'][0]['name']
|
||||
os.environ["AVALON_ASSET"] = entity['parent']['name']
|
||||
os.environ["AVALON_TASK"] = entity['name']
|
||||
os.environ["AVALON_APP"] = self.identifier.split("_")[0]
|
||||
os.environ["AVALON_APP_NAME"] = self.identifier
|
||||
|
||||
anatomy = pype.Anatomy
|
||||
hierarchy = database[project_name].find_one({
|
||||
"type": 'asset',
|
||||
"name": entity['parent']['name']
|
||||
})['data']['parents']
|
||||
|
||||
if hierarchy:
|
||||
hierarchy = os.path.join(*hierarchy)
|
||||
|
||||
data = {"project": {"name": entity['project']['full_name'],
|
||||
"code": entity['project']['name']},
|
||||
"task": entity['name'],
|
||||
"asset": entity['parent']['name'],
|
||||
"hierarchy": hierarchy}
|
||||
try:
|
||||
anatomy = anatomy.format(data)
|
||||
except Exception as e:
|
||||
self.log.error(
|
||||
"{0} Error in anatomy.format: {1}".format(__name__, e)
|
||||
)
|
||||
os.environ["AVALON_WORKDIR"] = os.path.join(
|
||||
anatomy.work.root, anatomy.work.folder
|
||||
)
|
||||
|
||||
# collect all parents from the task
|
||||
parents = []
|
||||
for item in entity['link']:
|
||||
parents.append(session.get(item['type'], item['id']))
|
||||
|
||||
# collect all the 'environment' attributes from parents
|
||||
tools_attr = [os.environ["AVALON_APP"], os.environ["AVALON_APP_NAME"]]
|
||||
for parent in reversed(parents):
|
||||
# check if the attribute is empty, if not use it
|
||||
if parent['custom_attributes']['tools_env']:
|
||||
tools_attr.extend(parent['custom_attributes']['tools_env'])
|
||||
break
|
||||
|
||||
tools_env = acre.get_tools(tools_attr)
|
||||
env = acre.compute(tools_env)
|
||||
env = acre.merge(env, current_env=dict(os.environ))
|
||||
|
||||
# Get path to execute
|
||||
st_temp_path = os.environ['PYPE_STUDIO_TEMPLATES']
|
||||
os_plat = platform.system().lower()
|
||||
|
||||
# Path to folder with launchers
|
||||
path = os.path.join(st_temp_path, 'bin', os_plat)
|
||||
# Full path to executable launcher
|
||||
execfile = None
|
||||
|
||||
if sys.platform == "win32":
|
||||
|
||||
for ext in os.environ["PATHEXT"].split(os.pathsep):
|
||||
fpath = os.path.join(path.strip('"'), self.executable + ext)
|
||||
if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
|
||||
execfile = fpath
|
||||
break
|
||||
pass
|
||||
|
||||
# Run SW if was found executable
|
||||
if execfile is not None:
|
||||
avalonlib.launch(executable=execfile, args=[], environment=env)
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'message': "We didn't found launcher for {0}"
|
||||
.format(self.label)
|
||||
}
|
||||
pass
|
||||
|
||||
if sys.platform.startswith('linux'):
|
||||
execfile = os.path.join(path.strip('"'), self.executable)
|
||||
if os.path.isfile(execfile):
|
||||
try:
|
||||
fp = open(execfile)
|
||||
except PermissionError as p:
|
||||
self.log.error('Access denied on {0} - {1}'.format(
|
||||
execfile, p))
|
||||
return {
|
||||
'success': False,
|
||||
'message': "Access denied on launcher - {}".format(
|
||||
execfile)
|
||||
}
|
||||
fp.close()
|
||||
# check executable permission
|
||||
if not os.access(execfile, os.X_OK):
|
||||
self.log.error('No executable permission on {}'.format(
|
||||
execfile))
|
||||
return {
|
||||
'success': False,
|
||||
'message': "No executable permission - {}".format(
|
||||
execfile)
|
||||
}
|
||||
pass
|
||||
else:
|
||||
self.log.error('Launcher doesn\'t exist - {}'.format(
|
||||
execfile))
|
||||
return {
|
||||
'success': False,
|
||||
'message': "Launcher doesn't exist - {}".format(execfile)
|
||||
}
|
||||
pass
|
||||
# Run SW if was found executable
|
||||
if execfile is not None:
|
||||
avalonlib.launch(
|
||||
'/usr/bin/env', args=['bash', execfile], environment=env
|
||||
)
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'message': "We didn't found launcher for {0}"
|
||||
.format(self.label)
|
||||
}
|
||||
pass
|
||||
|
||||
# RUN TIMER IN FTRACK
|
||||
username = event['source']['user']['username']
|
||||
user_query = 'User where username is "{}"'.format(username)
|
||||
user = session.query(user_query).one()
|
||||
task = session.query('Task where id is {}'.format(entity['id'])).one()
|
||||
self.log.info('Starting timer for task: ' + task['name'])
|
||||
user.start_timer(task, force=True)
|
||||
|
||||
# Change status of task to In progress
|
||||
config = get_config_data()
|
||||
|
||||
if (
|
||||
'status_on_app_launch' in config and
|
||||
'sync_to_avalon' in config and
|
||||
'statuses_name_change' in config['sync_to_avalon']
|
||||
):
|
||||
statuses = config['sync_to_avalon']['statuses_name_change']
|
||||
if entity['status']['name'].lower() in statuses:
|
||||
status_name = config['status_on_app_launch']
|
||||
|
||||
try:
|
||||
query = 'Status where name is "{}"'.format(status_name)
|
||||
status = session.query(query).one()
|
||||
task['status'] = status
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
msg = (
|
||||
'Status "{}" in config wasn\'t found on Ftrack'
|
||||
).format(status_name)
|
||||
self.log.warning(msg)
|
||||
|
||||
# Set origin avalon environments
|
||||
for key, value in env_origin.items():
|
||||
os.environ[key] = value
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'message': "Launching {0}".format(self.label)
|
||||
}
|
||||
312
pype/ftrack/lib/ftrack_base_handler.py
Normal file
312
pype/ftrack/lib/ftrack_base_handler.py
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
import ftrack_api
|
||||
import functools
|
||||
import time
|
||||
from pype import api as pype
|
||||
|
||||
|
||||
class BaseHandler(object):
|
||||
'''Custom Action base class
|
||||
|
||||
<label> - a descriptive string identifing your action.
|
||||
<varaint> - To group actions together, give them the same
|
||||
label and specify a unique variant per action.
|
||||
<identifier> - a unique identifier for app.
|
||||
<description> - a verbose descriptive text for you action
|
||||
<icon> - icon in ftrack
|
||||
'''
|
||||
# Default priority is 100
|
||||
priority = 100
|
||||
# Type is just for logging purpose (e.g.: Action, Event, Application,...)
|
||||
type = 'No-type'
|
||||
|
||||
def __init__(self, session):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
self._session = session
|
||||
self.log = pype.Logger.getLogger(self.__class__.__name__)
|
||||
|
||||
# Using decorator
|
||||
self.register = self.register_log(self.register)
|
||||
|
||||
# Decorator
|
||||
def register_log(self, func):
|
||||
@functools.wraps(func)
|
||||
def wrapper_register(*args, **kwargs):
|
||||
label = self.__class__.__name__
|
||||
if hasattr(self, 'label'):
|
||||
if self.variant is None:
|
||||
label = self.label
|
||||
else:
|
||||
label = '{} {}'.format(self.label, self.variant)
|
||||
|
||||
try:
|
||||
start_time = time.perf_counter()
|
||||
func(*args, **kwargs)
|
||||
end_time = time.perf_counter()
|
||||
run_time = end_time - start_time
|
||||
self.log.info((
|
||||
'{} "{}" - Registered successfully ({:.4f}sec)'
|
||||
).format(self.type, label, run_time))
|
||||
except NotImplementedError:
|
||||
self.log.error((
|
||||
'{} "{}" - Register method is not implemented'
|
||||
).format(
|
||||
self.type, label)
|
||||
)
|
||||
except Exception as e:
|
||||
self.log.error('{} "{}" - Registration failed ({})'.format(
|
||||
self.type, label, str(e))
|
||||
)
|
||||
return wrapper_register
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
'''Return current session.'''
|
||||
return self._session
|
||||
|
||||
def reset_session(self):
|
||||
self.session.reset()
|
||||
|
||||
def register(self):
|
||||
'''
|
||||
Registers the action, subscribing the discover and launch topics.
|
||||
Is decorated by register_log
|
||||
'''
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def _discover(self, event):
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
accepts = self.discover(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
if accepts:
|
||||
self.log.debug(u'Discovering action with selection: {0}'.format(
|
||||
args[1]['data'].get('selection', [])))
|
||||
return {
|
||||
'items': [{
|
||||
'label': self.label,
|
||||
'variant': self.variant,
|
||||
'description': self.description,
|
||||
'actionIdentifier': self.identifier,
|
||||
'icon': self.icon,
|
||||
}]
|
||||
}
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
'''Return true if we can handle the selected entities.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
|
||||
return False
|
||||
|
||||
def _translate_event(self, session, event):
|
||||
'''Return *event* translated structure to be used with the API.'''
|
||||
|
||||
'''Return *event* translated structure to be used with the API.'''
|
||||
|
||||
_selection = event['data'].get('selection', [])
|
||||
|
||||
_entities = list()
|
||||
for entity in _selection:
|
||||
_entities.append(
|
||||
(
|
||||
session.get(
|
||||
self._get_entity_type(entity),
|
||||
entity.get('entityId')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
_entities,
|
||||
event
|
||||
]
|
||||
|
||||
def _get_entity_type(self, entity):
|
||||
'''Return translated entity type tht can be used with API.'''
|
||||
# Get entity type and make sure it is lower cased. Most places except
|
||||
# the component tab in the Sidebar will use lower case notation.
|
||||
entity_type = entity.get('entityType').replace('_', '').lower()
|
||||
|
||||
for schema in self.session.schemas:
|
||||
alias_for = schema.get('alias_for')
|
||||
|
||||
if (
|
||||
alias_for and isinstance(alias_for, str) and
|
||||
alias_for.lower() == entity_type
|
||||
):
|
||||
return schema['id']
|
||||
|
||||
for schema in self.session.schemas:
|
||||
if schema['id'].lower() == entity_type:
|
||||
return schema['id']
|
||||
|
||||
raise ValueError(
|
||||
'Unable to translate entity type: {0}.'.format(entity_type)
|
||||
)
|
||||
|
||||
def _launch(self, event):
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
interface = self._interface(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
if interface:
|
||||
return interface
|
||||
|
||||
response = self.launch(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
return self._handle_result(
|
||||
self.session, response, *args
|
||||
)
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
'''Callback method for the custom action.
|
||||
|
||||
return either a bool ( True if successful or False if the action failed )
|
||||
or a dictionary with they keys `message` and `success`, the message should be a
|
||||
string and will be displayed as feedback to the user, success should be a bool,
|
||||
True if successful or False if the action failed.
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
def _interface(self, *args):
|
||||
interface = self.interface(*args)
|
||||
if interface:
|
||||
if 'items' in interface:
|
||||
return interface
|
||||
|
||||
return {
|
||||
'items': interface
|
||||
}
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
'''Return a interface if applicable or None
|
||||
|
||||
*session* is a `ftrack_api.Session` instance
|
||||
|
||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
||||
If the entity is a hierarchical you will always get the entity
|
||||
type TypedContext, once retrieved through a get operation you
|
||||
will have the "real" entity type ie. example Shot, Sequence
|
||||
or Asset Build.
|
||||
|
||||
*event* the unmodified original event
|
||||
'''
|
||||
return None
|
||||
|
||||
def _handle_result(self, session, result, entities, event):
|
||||
'''Validate the returned result from the action callback'''
|
||||
if isinstance(result, bool):
|
||||
result = {
|
||||
'success': result,
|
||||
'message': (
|
||||
'{0} launched successfully.'.format(
|
||||
self.label
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
elif isinstance(result, dict):
|
||||
for key in ('success', 'message'):
|
||||
if key in result:
|
||||
continue
|
||||
|
||||
raise KeyError(
|
||||
'Missing required key: {0}.'.format(key)
|
||||
)
|
||||
|
||||
else:
|
||||
self.log.error(
|
||||
'Invalid result type must be bool or dictionary!'
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def show_message(self, event, input_message, result=False):
|
||||
"""
|
||||
Shows message to user who triggered event
|
||||
- event - just source of user id
|
||||
- input_message - message that is shown to user
|
||||
- result - changes color of message (based on ftrack settings)
|
||||
- True = Violet
|
||||
- False = Red
|
||||
"""
|
||||
if not isinstance(result, bool):
|
||||
result = False
|
||||
|
||||
try:
|
||||
message = str(input_message)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
user_id = event['source']['user']['id']
|
||||
target = (
|
||||
'applicationId=ftrack.client.web and user.id="{0}"'
|
||||
).format(user_id)
|
||||
self.session.event_hub.publish(
|
||||
ftrack_api.event.base.Event(
|
||||
topic='ftrack.action.trigger-user-interface',
|
||||
data=dict(
|
||||
type='message',
|
||||
success=result,
|
||||
message=message
|
||||
),
|
||||
target=target
|
||||
),
|
||||
on_error='ignore'
|
||||
)
|
||||
|
||||
def show_interface(self, event, items, title=''):
|
||||
"""
|
||||
Shows interface to user who triggered event
|
||||
- 'items' must be list containing Ftrack interface items
|
||||
"""
|
||||
user_id = event['source']['user']['id']
|
||||
target = (
|
||||
'applicationId=ftrack.client.web and user.id="{0}"'
|
||||
).format(user_id)
|
||||
|
||||
self.session.event_hub.publish(
|
||||
ftrack_api.event.base.Event(
|
||||
topic='ftrack.action.trigger-user-interface',
|
||||
data=dict(
|
||||
type='widget',
|
||||
items=items,
|
||||
title=title
|
||||
),
|
||||
target=target
|
||||
),
|
||||
on_error='ignore'
|
||||
)
|
||||
60
pype/ftrack/lib/ftrack_event_handler.py
Normal file
60
pype/ftrack/lib/ftrack_event_handler.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
from .ftrack_base_handler import BaseHandler
|
||||
|
||||
|
||||
class BaseEvent(BaseHandler):
|
||||
'''Custom Event base class
|
||||
|
||||
BaseEvent is based on ftrack.update event
|
||||
- get entities from event
|
||||
|
||||
If want to use different event base
|
||||
- override register and *optional _translate_event method
|
||||
|
||||
'''
|
||||
|
||||
type = 'Event'
|
||||
|
||||
def __init__(self, session):
|
||||
'''Expects a ftrack_api.Session instance'''
|
||||
super().__init__(session)
|
||||
|
||||
def register(self):
|
||||
'''Registers the event, subscribing the discover and launch topics.'''
|
||||
self.session.event_hub.subscribe(
|
||||
'topic=ftrack.update',
|
||||
self._launch,
|
||||
priority=self.priority
|
||||
)
|
||||
|
||||
def _launch(self, event):
|
||||
args = self._translate_event(
|
||||
self.session, event
|
||||
)
|
||||
|
||||
self.launch(
|
||||
self.session, *args
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
def _translate_event(self, session, event):
|
||||
'''Return *event* translated structure to be used with the API.'''
|
||||
_selection = event['data'].get('entities', [])
|
||||
|
||||
_entities = list()
|
||||
for entity in _selection:
|
||||
if entity['entityType'] in ['socialfeed']:
|
||||
continue
|
||||
_entities.append(
|
||||
(
|
||||
session.get(
|
||||
self._get_entity_type(entity),
|
||||
entity.get('entityId')
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return [
|
||||
_entities,
|
||||
event
|
||||
]
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import sys
|
||||
import os
|
||||
import requests
|
||||
from app.vendor.Qt import QtCore, QtGui, QtWidgets
|
||||
|
|
@ -23,9 +22,9 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
|
||||
self.parent = parent
|
||||
|
||||
if hasattr(parent,'icon'):
|
||||
if hasattr(parent, 'icon'):
|
||||
self.setWindowIcon(self.parent.icon)
|
||||
elif hasattr(parent,'parent') and hasattr(parent.parent,'icon'):
|
||||
elif hasattr(parent, 'parent') and hasattr(parent.parent, 'icon'):
|
||||
self.setWindowIcon(self.parent.parent.icon)
|
||||
else:
|
||||
pype_setup = os.getenv('PYPE_SETUP_ROOT')
|
||||
|
|
@ -34,7 +33,10 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
icon = QtGui.QIcon(fname)
|
||||
self.setWindowIcon(icon)
|
||||
|
||||
self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint)
|
||||
self.setWindowFlags(
|
||||
QtCore.Qt.WindowCloseButtonHint |
|
||||
QtCore.Qt.WindowMinimizeButtonHint
|
||||
)
|
||||
|
||||
self.loginSignal.connect(self.loginWithCredentials)
|
||||
self._translate = QtCore.QCoreApplication.translate
|
||||
|
|
@ -85,7 +87,9 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
self.user_input.setEnabled(True)
|
||||
self.user_input.setFrame(True)
|
||||
self.user_input.setObjectName("user_input")
|
||||
self.user_input.setPlaceholderText(self._translate("main","user.name"))
|
||||
self.user_input.setPlaceholderText(
|
||||
self._translate("main", "user.name")
|
||||
)
|
||||
self.user_input.textChanged.connect(self._user_changed)
|
||||
|
||||
self.api_label = QtWidgets.QLabel("API Key:")
|
||||
|
|
@ -98,19 +102,21 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
self.api_input.setEnabled(True)
|
||||
self.api_input.setFrame(True)
|
||||
self.api_input.setObjectName("api_input")
|
||||
self.api_input.setPlaceholderText(self._translate("main","e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"))
|
||||
self.api_input.setPlaceholderText(self._translate(
|
||||
"main", "e.g. xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
))
|
||||
self.api_input.textChanged.connect(self._api_changed)
|
||||
|
||||
self.error_label = QtWidgets.QLabel("")
|
||||
self.error_label.setFont(self.font)
|
||||
self.error_label.setTextFormat(QtCore.Qt.RichText)
|
||||
self.error_label.setObjectName("error_label")
|
||||
self.error_label.setWordWrap(True);
|
||||
self.error_label.setWordWrap(True)
|
||||
self.error_label.hide()
|
||||
|
||||
self.form.addRow(self.ftsite_label, self.ftsite_input)
|
||||
self.form.addRow(self.user_label, self.user_input)
|
||||
self.form.addRow(self.api_label,self.api_input)
|
||||
self.form.addRow(self.api_label, self.api_input)
|
||||
self.form.addRow(self.error_label)
|
||||
|
||||
self.btnGroup = QtWidgets.QHBoxLayout()
|
||||
|
|
@ -118,7 +124,9 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
self.btnGroup.setObjectName("btnGroup")
|
||||
|
||||
self.btnEnter = QtWidgets.QPushButton("Login")
|
||||
self.btnEnter.setToolTip('Set Username and API Key with entered values')
|
||||
self.btnEnter.setToolTip(
|
||||
'Set Username and API Key with entered values'
|
||||
)
|
||||
self.btnEnter.clicked.connect(self.enter_credentials)
|
||||
|
||||
self.btnClose = QtWidgets.QPushButton("Close")
|
||||
|
|
@ -157,7 +165,7 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
|
||||
self.ftsite_input.setText(newurl)
|
||||
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
self.setError("FTRACK_SERVER is not set in templates")
|
||||
self.btnEnter.setEnabled(False)
|
||||
self.btnFtrack.setEnabled(False)
|
||||
|
|
@ -174,7 +182,7 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
def _api_changed(self):
|
||||
self.api_input.setStyleSheet("")
|
||||
|
||||
def _invalid_input(self,entity):
|
||||
def _invalid_input(self, entity):
|
||||
entity.setStyleSheet("border: 1px solid red;")
|
||||
|
||||
def enter_credentials(self):
|
||||
|
|
@ -205,11 +213,13 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
else:
|
||||
self._invalid_input(self.user_input)
|
||||
self._invalid_input(self.api_input)
|
||||
self.setError("We're unable to sign in to Ftrack with these credentials")
|
||||
self.setError(
|
||||
"We're unable to sign in to Ftrack with these credentials"
|
||||
)
|
||||
|
||||
def open_ftrack(self):
|
||||
url = self.ftsite_input.text()
|
||||
self.loginWithCredentials(url,None,None)
|
||||
self.loginWithCredentials(url, None, None)
|
||||
|
||||
def checkUrl(self, url):
|
||||
url = url.strip('/ ')
|
||||
|
|
@ -218,7 +228,7 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
self.setError("There is no URL set in Templates")
|
||||
return
|
||||
|
||||
if not 'http' in url:
|
||||
if 'http' not in url:
|
||||
if url.endswith('ftrackapp.com'):
|
||||
url = 'https://' + url
|
||||
else:
|
||||
|
|
@ -226,7 +236,8 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
try:
|
||||
result = requests.get(
|
||||
url,
|
||||
allow_redirects=False # Old python API will not work with redirect.
|
||||
# Old python API will not work with redirect.
|
||||
allow_redirects=False
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
self.setError(
|
||||
|
|
@ -234,7 +245,6 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
)
|
||||
return
|
||||
|
||||
|
||||
if (
|
||||
result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
|
||||
):
|
||||
|
|
@ -254,7 +264,7 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
)
|
||||
return
|
||||
|
||||
if not 'http' in url:
|
||||
if 'http' not in url:
|
||||
if url.endswith('ftrackapp.com'):
|
||||
url = 'https://' + url
|
||||
else:
|
||||
|
|
@ -262,7 +272,8 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
try:
|
||||
result = requests.get(
|
||||
url,
|
||||
allow_redirects=False # Old python API will not work with redirect.
|
||||
# Old python API will not work with redirect.
|
||||
allow_redirects=False
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
self.setError(
|
||||
|
|
@ -270,7 +281,6 @@ class Login_Dialog_ui(QtWidgets.QWidget):
|
|||
)
|
||||
return
|
||||
|
||||
|
||||
if (
|
||||
result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
|
||||
):
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib import parse
|
||||
import os
|
||||
import webbrowser
|
||||
import functools
|
||||
import pype
|
||||
import inspect
|
||||
from app.vendor.Qt import QtCore
|
||||
|
||||
# class LoginServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
|
||||
class LoginServerHandler(BaseHTTPRequestHandler):
|
||||
'''Login server handler.'''
|
||||
|
||||
|
|
@ -25,44 +28,21 @@ class LoginServerHandler(BaseHTTPRequestHandler):
|
|||
login_credentials = parse.parse_qs(query)
|
||||
api_user = login_credentials['api_user'][0]
|
||||
api_key = login_credentials['api_key'][0]
|
||||
message = """
|
||||
<html>
|
||||
<style type="text/css">
|
||||
|
||||
body {{
|
||||
background-color: #333;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
margin-top: 200px;
|
||||
}}
|
||||
|
||||
h1 {{
|
||||
font-family: "DejaVu Sans";
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}}
|
||||
|
||||
h3 {{
|
||||
font-weight: normal;
|
||||
font-family: "DejaVu Sans";
|
||||
margin: 30px 10px;
|
||||
}}
|
||||
|
||||
em {{
|
||||
color: #fff;
|
||||
}}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Sign in to Ftrack was successful</h1>
|
||||
<h3>
|
||||
You signed in with username <em>{0}</em>.
|
||||
</h3>
|
||||
<h3>
|
||||
You can close this window now.
|
||||
</h3>
|
||||
</body>
|
||||
</html>
|
||||
""".format(api_user)
|
||||
# get path to resources
|
||||
path_items = os.path.dirname(
|
||||
inspect.getfile(pype)
|
||||
).split(os.path.sep)
|
||||
del path_items[-1]
|
||||
path_items.extend(['res', 'ftrack', 'sign_in_message.html'])
|
||||
message_filepath = os.path.sep.join(path_items)
|
||||
message_file = open(message_filepath, 'r')
|
||||
sign_in_message = message_file.read()
|
||||
message_file.close()
|
||||
# formatting html code for python
|
||||
replacement = [('{', '{{'), ('}', '}}'), ('{{}}', '{}')]
|
||||
for r in (replacement):
|
||||
sign_in_message = sign_in_message.replace(*r)
|
||||
message = sign_in_message.format(api_user)
|
||||
else:
|
||||
message = '<h1>Failed to sign in</h1>'
|
||||
|
||||
|
|
@ -70,7 +50,6 @@ class LoginServerHandler(BaseHTTPRequestHandler):
|
|||
self.end_headers()
|
||||
self.wfile.write(message.encode())
|
||||
|
||||
|
||||
if login_credentials:
|
||||
self.login_callback(
|
||||
api_user,
|
||||
|
|
@ -84,7 +63,6 @@ class LoginServerThread(QtCore.QThread):
|
|||
# Login signal.
|
||||
loginSignal = QtCore.Signal(object, object, object)
|
||||
|
||||
|
||||
def start(self, url):
|
||||
'''Start thread.'''
|
||||
self.url = url
|
||||
|
|
@ -103,8 +81,11 @@ class LoginServerThread(QtCore.QThread):
|
|||
LoginServerHandler, self._handle_login
|
||||
)
|
||||
)
|
||||
unformated_url = (
|
||||
'{0}/user/api_credentials?''redirect_url=http://localhost:{1}'
|
||||
)
|
||||
webbrowser.open_new_tab(
|
||||
'{0}/user/api_credentials?redirect_url=http://localhost:{1}'.format(
|
||||
unformated_url.format(
|
||||
self.url, self._server.server_port
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -459,3 +459,10 @@ def get_all_avalon_projects():
|
|||
for name in project_names:
|
||||
projects.append(db[name].find_one({'type': 'project'}))
|
||||
return projects
|
||||
|
||||
|
||||
def get_presets_path():
|
||||
templates = os.environ['PYPE_STUDIO_TEMPLATES']
|
||||
path_items = [templates, 'presets']
|
||||
filepath = os.path.sep.join(path_items)
|
||||
return filepath
|
||||
|
|
|
|||
32
res/ftrack/sign_in_message.html
Normal file
32
res/ftrack/sign_in_message.html
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<html>
|
||||
<style type="text/css">
|
||||
body {
|
||||
background-color: #333;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
margin-top: 200px;
|
||||
}
|
||||
h1 {
|
||||
font-family: "DejaVu Sans";
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
font-family: "DejaVu Sans";
|
||||
margin: 30px 10px;
|
||||
}
|
||||
em {
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h1>Sign in to Ftrack was successful</h1>
|
||||
<h3>
|
||||
You signed in with username <em>{}</em>.
|
||||
</h3>
|
||||
<h3>
|
||||
You can close this window now.
|
||||
</h3>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue