ayon-core/pype/ftrack/lib/avalon_sync.py

671 lines
21 KiB
Python

import os
import re
import json
from pype.lib import get_avalon_database
from bson.objectid import ObjectId
import avalon
import avalon.api
from avalon import schema
from avalon.vendor import toml, jsonschema
from pypeapp import Logger, Anatomy, config
ValidationError = jsonschema.ValidationError
log = Logger().get_logger(__name__)
def get_ca_mongoid():
# returns name of Custom attribute that stores mongo_id
return 'avalon_mongo_id'
def import_to_avalon(
session, entity, ft_project, av_project, custom_attributes
):
database = get_avalon_database()
project_name = ft_project['full_name']
output = {}
errors = []
entity_type = entity.entity_type
ent_path = "/".join([ent["name"] for ent in entity['link']])
log.debug("{} [{}] - Processing".format(ent_path, entity_type))
ca_mongoid = get_ca_mongoid()
# Validate if entity has custom attribute avalon_mongo_id
if ca_mongoid not in entity['custom_attributes']:
msg = (
'Custom attribute "{}" for "{}" is not created'
' or don\'t have set permissions for API'
).format(ca_mongoid, entity['name'])
log.error(msg)
errors.append({'Custom attribute error': msg})
output['errors'] = errors
return output
# Validate if entity name match REGEX in schema
try:
avalon_check_name(entity)
except ValidationError:
msg = (
"{} - name \"{}\" includes unsupported symbols"
" like \"dash\" or \"space\""
).format(ent_path, name)
log.error(msg)
errors.append({'Unsupported character': msg})
output['errors'] = errors
return output
entity_type = entity.entity_type
# Project ////////////////////////////////////////////////////////////////
if entity_type in ['Project']:
type = 'project'
proj_config = get_project_config(entity)
schema.validate(proj_config)
av_project_code = None
if av_project is not None and 'code' in av_project['data']:
av_project_code = av_project['data']['code']
ft_project_code = ft_project['name']
if av_project is None:
log.debug("{} - Creating project".format(project_name))
item = {
'schema': "avalon-core:project-2.0",
'type': type,
'name': project_name,
'data': dict(),
'config': proj_config,
'parent': None,
}
schema.validate(item)
database[project_name].insert_one(item)
av_project = database[project_name].find_one(
{'type': type}
)
elif (
av_project['name'] != project_name or
(
av_project_code is not None and
av_project_code != ft_project_code
)
):
msg = (
'You can\'t change {0} "{1}" to "{2}"'
', avalon wouldn\'t work properly!'
'\n{0} was changed back!'
)
if av_project['name'] != project_name:
entity['full_name'] = av_project['name']
errors.append(
{'Changed name error': msg.format(
'Project name', av_project['name'], project_name
)}
)
if (
av_project_code is not None and
av_project_code != ft_project_code
):
log.warning((
"{0} - Project code"
" is different in Avalon (\"{1}\")"
" that in Ftrack (\"{2}\")!"
" Trying to change it back in Ftrack to \"{1}\"."
).format(
ent_path, str(av_project_code), str(ft_project_code)
))
entity['name'] = av_project_code
errors.append(
{'Changed name error': msg.format(
'Project code', av_project_code, ft_project_code
)}
)
try:
session.commit()
log.info((
"{} - Project code was changed back to \"{}\""
).format(ent_path, str(av_project_code)))
except Exception:
log.error(
(
"{} - Couldn't change project code back to \"{}\"."
).format(ent_path, str(av_project_code)),
exc_info=True
)
output['errors'] = errors
return output
else:
# not override existing templates!
templates = av_project['config'].get('template', None)
if templates is not None:
for key, value in proj_config['template'].items():
if (
key in templates and
templates[key] is not None and
templates[key] != value
):
proj_config['template'][key] = templates[key]
projectId = av_project['_id']
data = get_data(
entity, session, custom_attributes
)
cur_data = av_project.get('data') or {}
enter_data = {}
for k, v in cur_data.items():
enter_data[k] = v
for k, v in data.items():
enter_data[k] = v
log.debug("{} - Updating data".format(ent_path))
database[project_name].update_many(
{'_id': ObjectId(projectId)},
{'$set': {
'name': project_name,
'config': proj_config,
'data': data
}}
)
entity['custom_attributes'][ca_mongoid] = str(projectId)
session.commit()
output['project'] = av_project
return output
# Asset - /////////////////////////////////////////////////////////////
if av_project is None:
result = import_to_avalon(
session, ft_project, ft_project, av_project, custom_attributes
)
if 'errors' in result:
output['errors'] = result['errors']
return output
elif 'project' not in result:
msg = 'During project import went something wrong'
errors.append({'Unexpected error': msg})
output['errors'] = errors
return output
av_project = result['project']
output['project'] = result['project']
projectId = av_project['_id']
data = get_data(
entity, session, custom_attributes
)
# 1. hierarchical entity have silo set to None
silo = None
if len(data['parents']) > 0:
silo = data['parents'][0]
name = entity['name']
avalon_asset = None
# existence of this custom attr is already checked
if ca_mongoid not in entity['custom_attributes']:
msg = (
"Entity type \"{}\" don't have created custom attribute \"{}\""
" or user \"{}\" don't have permissions to read or change it."
).format(entity_type, ca_mongoid, session.api_user)
log.error(msg)
errors.append({'Missing Custom attribute': msg})
output['errors'] = errors
return output
mongo_id = entity['custom_attributes'][ca_mongoid]
mongo_id = mongo_id.replace(' ', '').replace('\n', '')
try:
ObjectId(mongo_id)
except Exception:
mongo_id = ''
if mongo_id != '':
avalon_asset = database[project_name].find_one(
{'_id': ObjectId(mongo_id)}
)
if avalon_asset is None:
avalon_asset = database[project_name].find_one(
{'type': 'asset', 'name': name}
)
if avalon_asset is None:
item = {
'schema': "avalon-core:asset-2.0",
'name': name,
'silo': silo,
'parent': ObjectId(projectId),
'type': 'asset',
'data': data
}
schema.validate(item)
mongo_id = database[project_name].insert_one(item).inserted_id
log.debug("{} - Created in project \"{}\"".format(
ent_path, project_name
))
# Raise error if it seems to be different ent. with same name
elif (
avalon_asset['data']['parents'] != data['parents'] or
avalon_asset['silo'] != silo
):
db_asset_path_items = [project_name,]
db_asset_path_items.extend(avalon_asset['data']['parents'])
db_asset_path_items.append(name)
msg = (
"{} - In Avalon DB already exists entity with name \"{}\""
"\n- \"{}\""
).format(ent_path, name, "/".join(db_asset_path_items))
log.error(msg)
errors.append({'Entity name duplication': msg})
output['errors'] = errors
return output
# Store new ID (in case that asset was removed from DB)
else:
mongo_id = avalon_asset['_id']
else:
if avalon_asset['name'] != entity['name']:
if silo is None or changeability_check_childs(entity) is False:
msg = (
'{} - You can\'t change name "{}" to "{}"'
', avalon wouldn\'t work properly!'
'\n\nName was changed back!'
'\n\nCreate new entity if you want to change name.'
).format(ent_path, avalon_asset['name'], entity['name'])
log.warning(msg)
entity['name'] = avalon_asset['name']
session.commit()
errors.append({'Changed name error': msg})
if (
avalon_asset['silo'] != silo or
avalon_asset['data']['parents'] != data['parents']
):
old_path = '/'.join(avalon_asset['data']['parents'])
new_path = '/'.join(data['parents'])
msg = (
'You can\'t move with entities.'
'\nEntity "{}" was moved from "{}" to "{}"'
'\n\nAvalon won\'t work properly, {}!'
)
moved_back = False
if 'visualParent' in avalon_asset['data']:
if silo is None:
asset_parent_id = avalon_asset['parent']
else:
asset_parent_id = avalon_asset['data']['visualParent']
asset_parent = database[project_name].find_one(
{'_id': ObjectId(asset_parent_id)}
)
ft_parent_id = asset_parent['data']['ftrackId']
try:
entity['parent_id'] = ft_parent_id
session.commit()
msg = msg.format(
avalon_asset['name'], old_path, new_path,
'entity was moved back'
)
log.warning(msg)
moved_back = True
except Exception:
moved_back = False
if moved_back is False:
msg = msg.format(
avalon_asset['name'], old_path, new_path,
'please move it back'
)
log.error(msg)
errors.append({'Hierarchy change error': msg})
if len(errors) > 0:
output['errors'] = errors
return output
avalon_asset = database[project_name].find_one(
{'_id': ObjectId(mongo_id)}
)
cur_data = avalon_asset.get('data') or {}
enter_data = {}
for k, v in cur_data.items():
enter_data[k] = v
for k, v in data.items():
enter_data[k] = v
database[project_name].update_many(
{'_id': ObjectId(mongo_id)},
{'$set': {
'name': name,
'silo': silo,
'data': enter_data,
'parent': ObjectId(projectId)
}})
log.debug("{} - Updated data (in project \"{}\")".format(
ent_path, project_name
))
entity['custom_attributes'][ca_mongoid] = str(mongo_id)
session.commit()
return output
def get_avalon_attr(session, split_hierarchical=False):
custom_attributes = []
hier_custom_attributes = []
cust_attrs_query = (
"select id, entity_type, object_type_id, is_hierarchical"
" from CustomAttributeConfiguration"
" where group.name = \"avalon\""
)
all_avalon_attr = session.query(cust_attrs_query).all()
for cust_attr in all_avalon_attr:
if 'avalon_' in cust_attr['key']:
continue
if split_hierarchical:
if cust_attr["is_hierarchical"]:
hier_custom_attributes.append(cust_attr)
continue
custom_attributes.append(cust_attr)
if split_hierarchical:
# return tuple
return custom_attributes, hier_custom_attributes
return custom_attributes
def changeability_check_childs(entity):
if (entity.entity_type.lower() != 'task' and 'children' not in entity):
return True
childs = entity['children']
for child in childs:
if child.entity_type.lower() == 'task':
available_statuses = config.get_presets().get(
"ftrack", {}).get(
"ftrack_config", {}).get(
"sync_to_avalon", {}).get(
"statuses_name_change", []
)
ent_status = child['status']['name'].lower()
if ent_status not in available_statuses:
return False
# If not task go deeper
elif changeability_check_childs(child) is False:
return False
# If everything is allright
return True
def get_data(entity, session, custom_attributes):
database = get_avalon_database()
entity_type = entity.entity_type
if entity_type.lower() == 'project':
ft_project = entity
elif entity_type.lower() != 'project':
ft_project = entity['project']
av_project = get_avalon_project(ft_project)
project_name = ft_project['full_name']
data = {}
data['ftrackId'] = entity['id']
data['entityType'] = entity_type
ent_types_query = "select id, name from ObjectType"
ent_types = session.query(ent_types_query).all()
ent_types_by_name = {
ent_type["name"]: ent_type["id"] for ent_type in ent_types
}
for cust_attr in custom_attributes:
# skip hierarchical attributes
if cust_attr.get('is_hierarchical', False):
continue
key = cust_attr['key']
if cust_attr['entity_type'].lower() in ['asset']:
data[key] = entity['custom_attributes'][key]
elif (
cust_attr['entity_type'].lower() in ['show'] and
entity_type.lower() == 'project'
):
data[key] = entity['custom_attributes'][key]
elif (
cust_attr['entity_type'].lower() in ['task'] and
entity_type.lower() != 'project'
):
# Put space between capitals (e.g. 'AssetBuild' -> 'Asset Build')
entity_type_full = re.sub(r"(\w)([A-Z])", r"\1 \2", entity_type)
# Get object id of entity type
ent_obj_type_id = ent_types_by_name.get(entity_type_full)
# Backup soluction when id is not found by prequeried objects
if not ent_obj_type_id:
query = 'ObjectType where name is "{}"'.format(
entity_type_full
)
ent_obj_type_id = session.query(query).one()['id']
if cust_attr['object_type_id'] == ent_obj_type_id:
if key in entity['custom_attributes']:
data[key] = entity['custom_attributes'][key]
if entity_type in ['Project']:
data['code'] = entity['name']
return data
# Get info for 'Data' in Avalon DB
tasks = []
for child in entity['children']:
if child.entity_type in ['Task']:
tasks.append(child['name'])
# Get list of parents without project
parents = []
folderStruct = []
for i in range(1, len(entity['link'])-1):
parEnt = session.get(
entity['link'][i]['type'],
entity['link'][i]['id']
)
parName = parEnt['name']
folderStruct.append(parName)
parents.append(parEnt)
parentId = None
for parent in parents:
parentId = database[project_name].find_one(
{'type': 'asset', 'name': parName}
)['_id']
if parent['parent'].entity_type != 'project' and parentId is None:
import_to_avalon(
session, parent, ft_project, av_project, custom_attributes
)
parentId = database[project_name].find_one(
{'type': 'asset', 'name': parName}
)['_id']
hierarchy = ""
if len(folderStruct) > 0:
hierarchy = os.path.sep.join(folderStruct)
data['visualParent'] = parentId
data['parents'] = folderStruct
data['tasks'] = tasks
data['hierarchy'] = hierarchy
return data
def get_avalon_project(ft_project):
database = get_avalon_database()
project_name = ft_project['full_name']
ca_mongoid = get_ca_mongoid()
if ca_mongoid not in ft_project['custom_attributes']:
return None
# try to find by Id
project_id = ft_project['custom_attributes'][ca_mongoid]
try:
avalon_project = database[project_name].find_one({
'_id': ObjectId(project_id)
})
except Exception:
avalon_project = None
if avalon_project is None:
avalon_project = database[project_name].find_one({
'type': 'project'
})
return avalon_project
def get_avalon_project_template():
"""Get avalon template
Returns:
dictionary with templates
"""
templates = Anatomy().templates
return {
'workfile': templates["avalon"]["workfile"],
'work': templates["avalon"]["work"],
'publish': templates["avalon"]["publish"]
}
def get_project_config(entity):
proj_config = {}
proj_config['schema'] = 'avalon-core:config-1.0'
proj_config['tasks'] = get_tasks(entity)
proj_config['apps'] = get_project_apps(entity)
proj_config['template'] = get_avalon_project_template()
return proj_config
def get_tasks(project):
task_types = project['project_schema']['_task_type_schema']['types']
return [{'name': task_type['name']} for task_type in task_types]
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:
toml_path = avalon.lib.which_app(app)
if not toml_path:
log.warning((
'Missing config file for application "{}"'
).format(app))
continue
apps.append({
'name': app,
'label': toml.load(toml_path)['label']
})
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 = "avalon-core:project-2.0"
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 show_errors(obj, event, errors):
title = 'Hey You! You raised few Errors! (*look below*)'
items = []
splitter = {'type': 'label', 'value': '---'}
for error in errors:
for key, message in error.items():
error_title = {
'type': 'label',
'value': '# {}'.format(key)
}
error_message = {
'type': 'label',
'value': '<p>{}</p>'.format(message)
}
if len(items) > 0:
items.append(splitter)
items.append(error_title)
items.append(error_message)
obj.log.error(
'{}: {}'.format(key, message)
)
obj.show_interface(items, title, event=event)