Sync updates also Tasks and Apps from Ftrack && dynamical action creating based on Avalon DB

This commit is contained in:
Jakub Trllo 2018-10-25 15:43:17 +02:00
parent 129ac28b21
commit f951bf5405
5 changed files with 95 additions and 308 deletions

View file

@ -5,7 +5,7 @@ from ftrack_action_handler.appaction import AppAction
from avalon import io
os.environ['AVALON_PROJECTS']='tmp'
os.environ['AVALON_PROJECTS'] = 'tmp'
io.install()
projects = sorted(io.projects(), key=lambda x: x['name'])
io.uninstall()
@ -18,14 +18,17 @@ s = ftrack_api.Session(
)
def register(session):
apps=[]
actions = []
for project in projects:
os.environ['AVALON_PROJECTS'] = project['name']
os.environ['AVALON_PROJECT'] = project['name']
for app in project['config']['apps']:
actions.append(AppAction(session, project['name'], app['label'], app['name']))
for action in actions:
action.register()
print(actions)
if app not in apps:
apps.append(app)
for app in apps:
AppAction(session, app['label'], app['name']).register()
session.event_hub.wait()

View file

@ -8,21 +8,20 @@ import json
import ftrack_api
from ftrack_action_handler.action import BaseAction
from avalon import io, inventory, schema
from avalon import io, inventory, lib
from avalon.vendor import toml
class SyncToAvalon(BaseAction):
'''Edit meta data action.'''
#: Action identifier.
identifier = 'sync.to.avalon'
#: Action label.
label = 'SyncToAvalon'
#: Action description.
description = 'Send data from Ftrack to Avalon'
#: Action icon.
icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/699650-icon-92-inbox-download-512.png'
def validate_selection(self, session, entities):
'''Return if *entities* is a valid selection.'''
@ -67,24 +66,37 @@ class SyncToAvalon(BaseAction):
# set AVALON_PROJECT env
os.environ["AVALON_PROJECT"] = entityProj["full_name"]
# get schema of project TODO read different schemas based on project type
# Get apps from Ftrack / TODO Exceptions?!!!
apps = []
for app in entityProj['custom_attributes']['applications']:
try:
label = toml.load(lib.which_app(app))['label']
apps.append({'name':app, 'label':label})
except Exception as e:
print('Error with application {0} - {1}'.format(app, e))
# Set project Config
config = {
'schema': 'avalon-core:config-1.0',
'tasks': [{'name': ''}],
'apps': apps,
'template': {'work': '','publish':''}
}
# Set project template
template = {"schema": "avalon-core:inventory-1.0"}
schema = entityProj['project_schema']['name']
fname = os.path.join(os.path.dirname(
os.path.realpath(__file__)),
(schema + '.toml'))
try:
with open(fname) as f:
config = toml.load(f)
except IOError:
raise
# --- Create project and assets in Avalon ---
os.environ['AVALON_ASSET'] = entityProj['full_name']
io.install()
# Check if project exists -> Create project
# If project don't exists -> <Create project> ELSE <Update Config>
if (io.find_one(
{"type": "project", "name": entityProj["full_name"]}) is None):
inventory.save(entityProj["full_name"], config, template)
{'type': 'project', 'name': entityProj['full_name']}) is None):
inventory.save(entityProj['full_name'], config, template)
else:
io.update_many({'type': 'project','name': entityProj['full_name']},
{'$set':{'config':config}})
# Store project Id
projectId = io.find_one({"type": "project", "name": entityProj["full_name"]})["_id"]
@ -105,25 +117,34 @@ class SyncToAvalon(BaseAction):
folderStruct = []
parentId = None
data = {'visualParent': parentId, 'parents': folderStruct,
'ftrackId': None, 'entityType': None}
'tasks':None, 'ftrackId': None, 'entityType': None}
for asset in assets:
os.environ['AVALON_ASSET'] = asset['name']
data.update({'ftrackId': asset['ftrackId'], 'entityType': asset['type']})
if (io.find_one({'type': 'asset', 'name': asset['name']}) is None):
inventory.create_asset(asset['name'], silo, data, projectId)
print("Asset "+asset['name']+" created")
# Get tasks of each asset
assetEnt = session.get('TypedContext', asset['ftrackId'])
tasks = []
for child in assetEnt['children']:
if child.entity_type in ['Task']:
tasks.append(child['name'])
data.update({'tasks': tasks})
if (io.find_one({'type': 'asset', 'name': asset['name']}) is None):
# Create asset in DB
inventory.create_asset(asset['name'], silo, data, projectId)
print("Asset "+asset['name']+" - created")
else:
io.update_many({'type': 'asset','name': asset['name']},
{'$set':{'data':data}})
# TODO check if is asset in same folder!!! ???? FEATURE FOR FUTURE
print("Asset "+asset["name"]+" already exist")
print("Asset "+asset["name"]+" - already exist")
parentId = io.find_one({'type': 'asset', 'name': asset['name']})['_id']
data.update({'visualParent': parentId, 'parents': folderStruct})
folderStruct.append(asset['name'])
# Set custom attribute to avalon/mongo id of entity (parentID is last)
if custAttrName in entity['custom_attributes'] and entity['custom_attributes'][custAttrName] is '':
entity['custom_attributes'][custAttrName] = str(parentId)
@ -144,6 +165,7 @@ class SyncToAvalon(BaseAction):
})
try:
print("action <" + self.__class__.__name__ + "> is running")
#TODO It's better to have these env set, are they used anywhere?
os.environ['AVALON_PROJECTS'] = "tmp"
os.environ['AVALON_ASSET'] = "tmp"
@ -173,7 +195,7 @@ class SyncToAvalon(BaseAction):
session.commit()
print('Synchronization to Avalon was successfull!')
except(e):
except Exception as e:
job['status'] = 'failed'
print('During synchronization to Avalon went something wrong!')
print(e)
@ -192,6 +214,7 @@ def register(session, **kw):
action_handler = SyncToAvalon(session)
action_handler.register()
print("----- action - <" + action_handler.__class__.__name__ + "> - Has been registered -----")
def main(arguments=None):

View file

@ -1,247 +0,0 @@
# :coding: utf-8
# :copyright: Copyright (c) 2017 ftrack
import sys
import argparse
import logging
import os
import json
import ftrack_api
from ftrack_action_handler.action import BaseAction
from avalon import io, inventory, schema
from avalon.vendor import toml
class SyncToAvalon(BaseAction):
'''Edit meta data action.'''
#: Action identifier.
identifier = 'sync.to.avalon'
#: Action label.
label = 'SyncToAvalon'
#: Action description.
description = 'Send data from Ftrack to Avalon'
#: Action icon.
icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/699650-icon-92-inbox-download-512.png'
def validate_selection(self, session, entities):
'''Return if *entities* is a valid selection.'''
# if (len(entities) != 1):
# # If entities contains more than one item return early since
# # metadata cannot be edited for several entites at the same time.
# return False
# entity_type, entity_id = entities[0]
# if (
# entity_type not in session.types
# ):
# # Return False if the target entity does not have a metadata
# # attribute.
# return False
pass
return True
def discover(self, session, entities, event):
'''Return True if action is valid.'''
self.logger.info('Got selection: {0}'.format(entities))
return self.validate_selection(session, entities)
def importToAvalon(self, session, entity):
eLinks = []
custAttrName = 'avalon_mongo_id'
# TODO read from file, which data are in scope???
# get needed info of entity and all parents
for e in entity['link']:
tmp = session.get(e['type'], e['id'])
if e['name'].find(" ") == -1:
name = e['name']
else:
name = e['name'].replace(" ", "-")
print("Name of "+tmp.entity_type+" - "+e['name']+" was changed to "+name)
eLinks.append({"type": tmp.entity_type, "name": name, "ftrackId": tmp['id']})
entityProj = session.get(eLinks[0]['type'], eLinks[0]['ftrackId'])
# set AVALON_PROJECT env
os.environ["AVALON_PROJECT"] = entityProj["full_name"]
# get schema of project TODO read different schemas based on project type
template = {"schema": "avalon-core:inventory-1.0"}
schema = entityProj['project_schema']['name']
fname = os.path.join(os.path.dirname(
os.path.realpath(__file__)),
(schema + '.toml'))
try:
with open(fname) as f:
config = toml.load(f)
except IOError:
raise
# --- Create project and assets in Avalon ---
io.install()
# Check if project exists -> Create project
if (io.find_one(
{"type": "project", "name": entityProj["full_name"]}) is None):
inventory.save(entityProj["full_name"], config, template)
# Store project Id
projectId = io.find_one({"type": "project", "name": entityProj["full_name"]})["_id"]
if custAttrName in entityProj['custom_attributes'] and entityProj['custom_attributes'][custAttrName] is '':
entityProj['custom_attributes'][custAttrName] = str(projectId)
# If entity is Project or have only 1 entity kill action
if (len(eLinks) > 1) and not (eLinks[-1]['type'] in ['Project']):
# TODO how to check if entity is Asset Library or AssetBuild?
silo = 'Assets' if eLinks[-1]['type'] in ['AssetBuild', 'Library'] else 'Film'
os.environ['AVALON_SILO'] = silo
# Create Assets
assets = []
for i in range(1, len(eLinks)):
assets.append(eLinks[i])
folderStruct = []
parentId = None
data = {'visualParent': parentId, 'parents': folderStruct,
'tasks':None, 'ftrackId': None, 'entityType': None}
for asset in assets:
os.environ['AVALON_ASSET'] = asset['name']
data.update({'ftrackId': asset['ftrackId'], 'entityType': asset['type']})
# Get tasks of each asset
assetEnt = session.get('TypedContext', asset['ftrackId'])
tasks = []
for child in assetEnt['children']:
if child.entity_type in ['Task']:
tasks.append(child['name'])
data.update({'tasks': tasks})
if (io.find_one({'type': 'asset', 'name': asset['name']}) is None):
# Create asset in DB
inventory.create_asset(asset['name'], silo, data, projectId)
print("Asset "+asset['name']+" - created")
else:
io.update_many({'type': 'asset','name': asset['name']},
{'$set':{'data':data}})
# TODO check if is asset in same folder!!! ???? FEATURE FOR FUTURE
print("Asset "+asset["name"]+" - already exist")
parentId = io.find_one({'type': 'asset', 'name': asset['name']})['_id']
data.update({'visualParent': parentId, 'parents': folderStruct})
folderStruct.append(asset['name'])
# Set custom attribute to avalon/mongo id of entity (parentID is last)
if custAttrName in entity['custom_attributes'] and entity['custom_attributes'][custAttrName] is '':
entity['custom_attributes'][custAttrName] = str(parentId)
io.uninstall()
def launch(self, session, entities, event):
# JOB SETTINGS
userId = event['source']['user']['id']
user = session.query('User where id is ' + userId).one()
job = session.create('Job', {
'user': user,
'status': 'running',
'data': json.dumps({
'description': 'Synch Ftrack to Avalon.'
})
})
try:
print("action <" + self.__class__.__name__ + "> is running")
#TODO It's better to have these env set, are they used anywhere?
os.environ['AVALON_PROJECTS'] = "tmp"
os.environ['AVALON_ASSET'] = "tmp"
os.environ['AVALON_SILO'] = "tmp"
importable = []
def getShotAsset(entity):
if not (entity.entity_type in ['Task']):
if entity not in importable:
importable.append(entity)
if entity['children']:
childrens = entity['children']
for child in childrens:
getShotAsset(child)
# get all entities separately
for entity in entities:
entity_type, entity_id = entity
act_ent = session.get(entity_type, entity_id)
getShotAsset(act_ent)
for e in importable:
self.importToAvalon(session, e)
job['status'] = 'done'
session.commit()
print('Synchronization to Avalon was successfull!')
except Exception as e:
job['status'] = 'failed'
print('During synchronization to Avalon went something wrong!')
print(e)
return True
def register(session, **kw):
'''Register plugin. Called when used as an plugin.'''
# Validate that session is an instance of ftrack_api.Session. If not,
# assume that register is being called from an old or incompatible API and
# return without doing anything.
if not isinstance(session, ftrack_api.session.Session):
return
action_handler = SyncToAvalon(session)
action_handler.register()
print("----- action - <" + action_handler.__class__.__name__ + "> - Has been registered -----")
def main(arguments=None):
'''Set up logging and register 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)
# Set up basic logging
logging.basicConfig(level=loggingLevels[namespace.verbosity])
session = ftrack_api.Session(
server_url="https://pype.ftrackapp.com",
api_key="4e01eda0-24b3-4451-8e01-70edc03286be",
)
register(session)
# Wait for events
logging.info(
'Registered actions and listening for events. Use Ctrl-C to abort.'
)
session.event_hub.wait()
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))

View file

@ -1,21 +1,6 @@
schema = "avalon-core:config-1.0"
[[tasks]]
name = "model"
[[tasks]]
name = "render"
[[tasks]]
name = "animate"
[[tasks]]
name = "rig"
[[tasks]]
name = "lookdev"
[[tasks]]
name = "layout"
name = ""
[[apps]]
name = "maya2018"
@ -26,5 +11,4 @@ name = "nuke11"
label = "The Foundry Nuke 11.0"
[template]
work = "{root}/{project}/{asset}/work/{task}/{app}"
publish = "{root}/{project}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}"
work = ""

View file

@ -1,10 +1,10 @@
# :coding: utf-8
# :copyright: Copyright (c) 2017 ftrack
import os
import logging
import getpass
import ftrack_api
from avalon import io
class AppAction(object):
'''Custom Action base class
@ -16,13 +16,8 @@ class AppAction(object):
<description> - a verbose descriptive text for you action
<icon> - icon in ftrack
'''
label = None
variant = None
identifier = None
description = None
icon = None
def __init__(self, session, project, label, name, variant=None, description=None, icon=None):
def __init__(self, session, label, name, icon=None, variant=None, description=None):
'''Expects a ftrack_api.Session instance'''
self.logger = logging.getLogger(
@ -33,13 +28,13 @@ class AppAction(object):
raise ValueError('Action missing label.')
elif name is None:
raise ValueError('Action missing identifier.')
elif project is None:
raise ValueError('Action missing project name.')
self._session = session
self.project = project
self.label = label
self.identifier = name
self.icon = None
self.variant = None
self.description = None
@property
@ -100,8 +95,37 @@ class AppAction(object):
*event* the unmodified original event
'''
if len(entities) > 1:
return False
return False
entity_type, entity_id = entities[0]
entity = session.get(entity_type, entity_id)
''' Should return False if not TASK ?!!! '''
# if entity.entity_type != 'Task':
# return False
ft_project = entity['project']
project = None
''' GET ONLY PROJECT INSTEAD OF ALL (io.find_one()) '''
os.environ['AVALON_PROJECTS'] = 'tmp'
io.install()
projects = sorted(io.projects(), key=lambda x: x['name'])
io.uninstall()
for p in projects:
if p['name'] == ft_project['full_name']:
project = p
break
os.environ['AVALON_PROJECT'] = project['name']
apps = []
for app in 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.'''