Merge branch 'develop' into feature/PYPE-14-ftrack-stop-timer

# Conflicts:
#	pype/ftrack/ftrack_run.py
This commit is contained in:
Milan Kolar 2018-12-09 18:09:27 +01:00
commit e5df6ad2b3
7 changed files with 442 additions and 147 deletions

View file

@ -9,7 +9,7 @@ from app.api import Logger
log = Logger.getLogger(__name__)
def registerApp(app, session):
name = app['name'].split("_")[0]
name = app['name']
variant = ""
try:
variant = app['name'].split("_")[1]
@ -31,11 +31,19 @@ def registerApp(app, session):
label = apptoml['ftrack_label']
icon = None
ftrack_resources = "" # Path to resources here
if 'icon' in apptoml:
icon = apptoml['icon']
if '{ftrack_resources}' in icon:
icon = icon.format(ftrack_resources)
description = None
if 'description' in apptoml:
description = apptoml['description']
# register action
AppAction(session, label, name, executable, variant, icon).register()
AppAction(session, label, name, executable, variant, icon, description).register()
def register(session):
@ -59,6 +67,7 @@ def register(session):
appNames.append(app['name'])
apps.append(app)
apps = sorted(apps, key=lambda x: x['name'])
for app in apps:
try:
registerApp(app, session)

View file

@ -48,9 +48,9 @@ class SyncToAvalon(BaseAction):
'''
#: Action identifier.
identifier = 'sync.to.avalon'
identifier = 'sync.to.avalon.local'
#: Action label.
label = 'SyncToAvalon'
label = 'SyncToAvalon - Local'
#: Action description.
description = 'Send data from Ftrack to Avalon'
#: Action icon.
@ -61,7 +61,7 @@ class SyncToAvalon(BaseAction):
''' Validation '''
roleCheck = False
discover = False
roleList = ['Administrator', 'Project Manager']
roleList = ['Pypeclub']
userId = event['source']['user']['id']
user = session.query('User where id is ' + userId).one()

View file

@ -14,9 +14,6 @@ import acre
from pype import api as pype
log = pype.Logger.getLogger(__name__, "ftrack")
log.debug("pype.Anatomy: {}".format(pype.Anatomy))
class AppAction(object):
@ -72,9 +69,7 @@ class AppAction(object):
),
self._launch
)
self.log.info("Application '{}' - Registered successfully".format(self.label))
self.log.info("Application '{}' - Registered successfully".format(self.label))
self.log.info("Application '{} {}' - Registered successfully".format(self.label,self.variant))
def _discover(self, event):
args = self._translate_event(
@ -137,7 +132,7 @@ class AppAction(object):
else:
apps = []
for app in project['config']['apps']:
apps.append(app['name'].split("_")[0])
apps.append(app['name'])
if self.identifier not in apps:
return False
@ -231,17 +226,13 @@ class AppAction(object):
entity, id = entities[0]
entity = session.get(entity, id)
silo = "Film"
if entity.entity_type == "AssetBuild":
silo = "Asset"
# set environments for Avalon
os.environ["AVALON_PROJECT"] = entity['project']['full_name']
os.environ["AVALON_SILO"] = silo
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
os.environ["AVALON_APP_NAME"] = self.identifier + "_" + self.variant
os.environ["AVALON_APP"] = self.identifier.split("_")[0]
os.environ["AVALON_APP_NAME"] = self.identifier
os.environ["FTRACK_TASKID"] = id
@ -262,7 +253,7 @@ class AppAction(object):
try:
anatomy = anatomy.format(data)
except Exception as e:
log.error("{0} Error in anatomy.format: {1}".format(__name__, 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)
# TODO Add paths to avalon setup from tomls
@ -282,7 +273,7 @@ class AppAction(object):
parents.append(session.get(item['type'], item['id']))
# collect all the 'environment' attributes from parents
tools_attr = [os.environ["AVALON_APP_NAME"]]
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']:
@ -328,7 +319,7 @@ class AppAction(object):
try:
fp = open(execfile)
except PermissionError as p:
log.error('Access denied on {0} - {1}'.
self.log.error('Access denied on {0} - {1}'.
format(execfile, p))
return {
'success': False,
@ -338,7 +329,7 @@ class AppAction(object):
fp.close()
# check executable permission
if not os.access(execfile, os.X_OK):
log.error('No executable permission on {}'.
self.log.error('No executable permission on {}'.
format(execfile))
return {
'success': False,
@ -347,7 +338,7 @@ class AppAction(object):
}
pass
else:
log.error('Launcher doesn\'t exist - {}'.
self.log.error('Launcher doesn\'t exist - {}'.
format(execfile))
return {
'success': False,

View file

@ -0,0 +1,36 @@
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 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:
self.login_widget.user_input.setText(cred['username'])
self.login_widget.api_input.setText(cred['apiKey'])
self.login_widget.setError("Credentials should be for API User")
self.login_widget.show()
def loginChange(self):
log.info("Logged successfully")
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

@ -0,0 +1,366 @@
import sys
import argparse
import logging
import os
import ftrack_api
import json
import re
from pype import lib
from pype.ftrack.actions.ftrack_action_handler import BaseAction
from bson.objectid import ObjectId
from avalon import io, inventory
from pype.ftrack import ftrack_utils
class Sync_To_Avalon(BaseAction):
'''
Synchronizing data action - from Ftrack to Avalon DB
Stores all information about entity.
- Name(string) - Most important information = identifier of entity
- Parent(ObjectId) - Avalon Project Id, if entity is not project itself
- Silo(string) - Last parent except project
- Data(dictionary):
- VisualParent(ObjectId) - Avalon Id of parent asset
- Parents(array of string) - All parent names except project
- Tasks(array of string) - Tasks on asset
- FtrackId(string)
- entityType(string) - entity's type on Ftrack
* All Custom attributes in group 'Avalon' which name don't start with 'avalon_'
* These information are stored also for all parents and children entities.
Avalon ID of asset is stored to Ftrack -> Custom attribute 'avalon_mongo_id'.
- action IS NOT creating this Custom attribute if doesn't exist
- run 'Create Custom Attributes' action or do it manually (Not recommended)
If Ftrack entity already has Custom Attribute 'avalon_mongo_id' that stores ID:
- name, parents and silo are checked -> shows error if are not exact the same
- after sync it is not allowed to change names or move entities
If ID in 'avalon_mongo_id' is empty string or is not found in DB:
- tries to find entity by name
- found:
- raise error if ftrackId/visual parent/parents are not same
- not found:
- Creates asset/project
'''
#: 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 register(self):
'''Registers the action, subscribing the the discover and launch topics.'''
self.session.event_hub.subscribe(
'topic=ftrack.action.discover',
self._discover
)
self.session.event_hub.subscribe(
'topic=ftrack.action.launch and data.actionIdentifier={0}'.format(
self.identifier
),
self._launch
)
self.log.info("Action '{}' - Registered successfully".format(self.__class__.__name__))
def discover(self, session, entities, event):
''' Validation '''
roleCheck = False
discover = False
roleList = ['Administrator', 'Project Manager']
userId = event['source']['user']['id']
user = session.query('User where id is ' + userId).one()
for role in user['user_security_roles']:
if role['security_role']['name'] in roleList:
roleCheck = True
if roleCheck is True:
for entity in entities:
if entity.entity_type.lower() not in ['task', 'assetversion']:
discover = True
break
return discover
def launch(self, session, entities, event):
message = ""
# 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:
self.log.info("Action <" + self.__class__.__name__ + "> is running")
self.ca_mongoid = 'avalon_mongo_id'
#TODO AVALON_PROJECTS, AVALON_ASSET, AVALON_SILO should be set up otherwise console log shows avalon debug
self.setAvalonAttributes()
self.importable = []
# get from top entity in hierarchy all parent entities
top_entity = entities[0]['link']
if len(top_entity) > 1:
for e in top_entity:
parent_entity = session.get(e['type'], e['id'])
self.importable.append(parent_entity)
# get all child entities separately/unique
for entity in entities:
self.getShotAsset(entity)
# Check names: REGEX in schema/duplicates - raise error if found
all_names = []
duplicates = []
for e in self.importable:
ftrack_utils.avalon_check_name(e)
if e['name'] in all_names:
duplicates.append("'{}'".format(e['name']))
else:
all_names.append(e['name'])
if len(duplicates) > 0:
raise ValueError("Entity name duplication: {}".format(", ".join(duplicates)))
## ----- PROJECT ------
# store Ftrack project- self.importable[0] must be project entity!!!
self.entityProj = self.importable[0]
# set AVALON_ env
os.environ["AVALON_PROJECT"] = self.entityProj["full_name"]
os.environ["AVALON_ASSET"] = self.entityProj["full_name"]
self.avalon_project = None
io.install()
# Import all entities to Avalon DB
for e in self.importable:
self.importToAvalon(session, e)
io.uninstall()
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(exc_type, fname, exc_tb.tb_lineno)
self.log.error('Error during syncToAvalon: {}'.format(log_message))
message = 'Unexpected Error - Please check Log for more information'
if len(message) > 0:
message = "Unable to sync: {}".format(message)
return {
'success': False,
'message': message
}
return {
'success': True,
'message': "Synchronization was successfull"
}
def setAvalonAttributes(self):
self.custom_attributes = []
all_avalon_attr = self.session.query('CustomAttributeGroup where name is "avalon"').one()
for cust_attr in all_avalon_attr['custom_attribute_configurations']:
if 'avalon_' not in cust_attr['key']:
self.custom_attributes.append(cust_attr)
def getShotAsset(self, entity):
if not (entity.entity_type in ['Task']):
if entity not in self.importable:
self.importable.append(entity)
if entity['children']:
childrens = entity['children']
for child in childrens:
self.getShotAsset(child)
def importToAvalon(self, session, entity):
# --- Begin: PUSH TO Avalon ---
entity_type = entity.entity_type
if entity_type.lower() in ['project']:
# Set project Config
config = ftrack_utils.get_config(entity)
# Set project template
template = lib.get_avalon_project_template_schema()
if self.ca_mongoid in entity['custom_attributes']:
try:
projectId = ObjectId(self.entityProj['custom_attributes'][self.ca_mongoid])
self.avalon_project = io.find_one({"_id": projectId})
except:
self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name']))
if self.avalon_project is None:
self.avalon_project = io.find_one({
"type": "project",
"name": entity["full_name"]
})
if self.avalon_project is None:
inventory.save(entity['full_name'], config, template)
self.avalon_project = io.find_one({
"type": "project",
"name": entity["full_name"]
})
elif self.avalon_project['name'] != entity['full_name']:
raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly!'.format(self.avalon_project['name'], name))
data = ftrack_utils.get_data(self, entity, session,self.custom_attributes)
# Store info about project (FtrackId)
io.update_many({
'type': 'project',
'name': entity['full_name']
}, {
'$set':{'data':data, 'config':config}
})
self.projectId = self.avalon_project["_id"]
if self.ca_mongoid in entity['custom_attributes']:
entity['custom_attributes'][self.ca_mongoid] = str(self.projectId)
else:
self.log.error('Custom attribute for "{}" is not created.'.format(entity['name']))
return
## ----- ASSETS ------
# Presets:
data = ftrack_utils.get_data(self, entity, session, self.custom_attributes)
# return if entity is silo
if len(data['parents']) == 0:
return
else:
silo = data['parents'][0]
os.environ['AVALON_SILO'] = silo
name = entity['name']
os.environ['AVALON_ASSET'] = name
# Try to find asset in current database
avalon_asset = None
if self.ca_mongoid in entity['custom_attributes']:
try:
entityId = ObjectId(entity['custom_attributes'][self.ca_mongoid])
avalon_asset = io.find_one({"_id": entityId})
except:
self.log.debug("Entity {} don't have stored entity id in ftrack".format(entity['name']))
if avalon_asset is None:
avalon_asset = io.find_one({'type': 'asset', 'name': name})
# Create if don't exists
if avalon_asset is None:
inventory.create_asset(name, silo, data, self.projectId)
self.log.debug("Asset {} - created".format(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):
raise ValueError('In Avalon DB already exists entity with name "{0}"'.format(name))
elif avalon_asset['name'] != entity['name']:
raise ValueError('You can\'t change name {} to {}, avalon DB won\'t work properly - please set name back'.format(avalon_asset['name'], name))
elif avalon_asset['silo'] != silo or avalon_asset['data']['parents'] != data['parents']:
old_path = "/".join(avalon_asset['data']['parents'])
new_path = "/".join(data['parents'])
raise ValueError('You can\'t move with entities. Entity "{}" was moved from "{}" to "{}" '.format(avalon_asset['name'], old_path, new_path))
# Update info
io.update_many({'type': 'asset','name': name},
{'$set':{'data':data, 'silo': silo}})
self.log.debug("Asset {} - updated".format(name))
entityId = io.find_one({'type': 'asset', 'name': name})['_id']
## FTRACK FEATURE - FTRACK MUST HAVE avalon_mongo_id FOR EACH ENTITY TYPE EXCEPT TASK
# Set custom attribute to avalon/mongo id of entity (parentID is last)
if self.ca_mongoid in entity['custom_attributes']:
entity['custom_attributes'][self.ca_mongoid] = str(entityId)
else:
self.log.error("Custom attribute for <{}> is not created.".format(entity['name']))
session.commit()
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 = Sync_To_Avalon(session)
action_handler.register()
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()
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

@ -23,9 +23,19 @@ class Login_Dialog_ui(QtWidgets.QWidget):
self.parent = parent
self.setWindowIcon(self.parent.parent.icon)
if hasattr(parent,'icon'):
self.setWindowIcon(self.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')
items = [pype_setup, "app", "resources", "icon.png"]
fname = os.path.sep.join(items)
icon = QtGui.QIcon(fname)
self.setWindowIcon(icon)
self.setWindowFlags(QtCore.Qt.WindowCloseButtonHint | QtCore.Qt.WindowMinimizeButtonHint)
self.loginSignal.connect(self.loginWithCredentials)
self._translate = QtCore.QCoreApplication.translate
@ -189,6 +199,8 @@ class Login_Dialog_ui(QtWidgets.QWidget):
if verification:
credentials._save_credentials(username, apiKey)
credentials._set_env(username, apiKey)
if self.parent is not None:
self.parent.loginChange()
self._close_widget()
else:
self._invalid_input(self.user_input)
@ -285,7 +297,8 @@ class Login_Dialog_ui(QtWidgets.QWidget):
if verification is True:
credentials._save_credentials(username, apiKey)
credentials._set_env(username, apiKey)
self.parent.loginChange()
if self.parent is not None:
self.parent.loginChange()
self._close_widget()
def closeEvent(self, event):

View file

@ -1,120 +0,0 @@
import os
import sys
import requests
import argparse
from pprint import pprint
from PyQt5 import QtCore, QtWidgets
from app import style
from . import credentials, login_tools
class Login_Dialog(QtWidgets.QWidget):
loginSignal = QtCore.pyqtSignal(object, object, object)
_login_server_thread = None
def __init__(self):
super().__init__()
self.loginSignal.connect(self.loginWithCredentials)
def run(self):
try:
url = os.getenv('FTRACK_SERVER')
except:
print("Environment variable 'FTRACK_SERVER' is not set.")
return
self.url = self.checkUrl(url)
self.open_ftrack()
def open_ftrack(self):
self.loginWithCredentials(self.url, None, None)
def checkUrl(self, url):
url = url.strip('/ ')
if not url:
print("Url is empty!")
return
if not 'http' in url:
if url.endswith('ftrackapp.com'):
url = 'https://' + url
else:
url = 'https://{0}.ftrackapp.com'.format(url)
try:
result = requests.get(
url,
allow_redirects=False # Old python API will not work with redirect.
)
except requests.exceptions.RequestException:
print('The server URL set in Templates could not be reached.')
return
if (
result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
):
print('The server URL set in Templates is not a valid ftrack server.')
return
return url
def loginWithCredentials(self, url, username, apiKey):
url = url.strip('/ ')
if not url:
print(
'You need to specify a valid server URL, '
'for example https://server-name.ftrackapp.com'
)
return
if not 'http' in url:
if url.endswith('ftrackapp.com'):
url = 'https://' + url
else:
url = 'https://{0}.ftrackapp.com'.format(url)
try:
result = requests.get(
url,
allow_redirects=False # Old python API will not work with redirect.
)
except requests.exceptions.RequestException:
print('The server URL you provided could not be reached.')
return
if (
result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
):
print('The server URL you provided is not a valid ftrack server.')
return
# If there is an existing server thread running we need to stop it.
if self._login_server_thread:
self._login_server_thread.quit()
self._login_server_thread = None
# If credentials are not properly set, try to get them using a http
# server.
if not username or not apiKey:
self._login_server_thread = login_tools.LoginServerThread()
self._login_server_thread.loginSignal.connect(self.loginSignal)
self._login_server_thread.start(url)
verification = credentials._check_credentials(username, apiKey)
if verification is True:
credentials._save_credentials(username, apiKey)
credentials._set_env(username, apiKey)
self.close()
def run_login():
app = QtWidgets.QApplication(sys.argv)
applogin = Login_Dialog()
applogin.run()
app.exec_()
if __name__ == '__main__':
run_login()