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:
Jakub Trllo 2019-01-28 17:22:51 +00:00 committed by Milan Kolar
commit df4f5b8b41
43 changed files with 1404 additions and 2181 deletions

View file

@ -1 +1 @@
from .lib import *

View file

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

View file

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

View file

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

View file

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

View file

@ -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]

View file

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

View file

@ -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'
}

View file

@ -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 = {}

View file

@ -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 {

View file

@ -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,

View file

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

View file

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

View file

@ -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:

View file

@ -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:

View file

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

View file

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

View file

@ -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:]))

View file

@ -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:

View file

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

View file

@ -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,

View file

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

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

View file

@ -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,

View file

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

View file

@ -1,5 +1,5 @@
import ftrack_api
from ftrack_event_handler import BaseEvent
from pype.ftrack import BaseEvent
class ThumbnailEvents(BaseEvent):

View file

@ -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:

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +0,0 @@
from .ftrack_utils import *
from .avalon_sync import *

View file

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

View 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 *

View file

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

View 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

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

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

View 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
]

View file

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

View file

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

View file

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

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