Merged in feature/pype-223_more_ftrack_cleanup (pull request #87)

Feature/PYPE-223 more ftrack cleanup

Approved-by: Milan Kolar <milan@orbi.tools>
This commit is contained in:
Jakub Trllo 2019-03-04 17:13:30 +00:00 committed by Milan Kolar
commit e04f2033dd
28 changed files with 500 additions and 179 deletions

View file

@ -1 +1,2 @@
from .lib import *
from .ftrack_server import *

View file

@ -87,8 +87,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
action_handler = AssetDelete(session)
action_handler.register()
AssetDelete(session).register()
def main(arguments=None):

View file

@ -111,6 +111,13 @@ class CustomAttributes(BaseAction):
label = 'Create/Update Avalon Attributes'
#: Action description.
description = 'Creates Avalon/Mongo ID for double check'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
icon = (
'https://cdn4.iconfinder.com/data/icons/'
'ios-web-user-interface-multi-circle-flat-vol-4/512/'
'Bullet_list_menu_lines_points_items_options-512.png'
)
def __init__(self, session):
super().__init__(session)
@ -576,23 +583,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
roleList = ['Pypeclub', 'Administrator']
username = session.api_user
user = session.query('User where username is "{}"'.format(username)).one()
available = False
for role in user['user_security_roles']:
if role['security_role']['name'] in roleList:
available = True
break
if available is True:
CustomAttributes(session).register()
else:
logging.info(
"!!! You're missing required permissions for action {}".format(
CustomAttributes.__name__
)
)
CustomAttributes(session).register()
def main(arguments=None):

View file

@ -13,10 +13,16 @@ class DeleteAsset(BaseAction):
#: Action identifier.
identifier = 'delete.asset'
#: Action label.
label = 'Delete asset/subsets'
label = 'Delete Asset/Subsets'
#: Action description.
description = 'Removes from Avalon with all childs and asset from Ftrack'
icon = "https://www.iconsdb.com/icons/preview/white/full-trash-xxl.png"
icon = (
'https://cdn4.iconfinder.com/data/icons/'
'ios-web-user-interface-multi-circle-flat-vol-5/512/'
'Delete_dustbin_empty_recycle_recycling_remove_trash-512.png'
)
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
#: Db
db = DbConnector()
@ -171,7 +177,7 @@ class DeleteAsset(BaseAction):
)
else:
title = title.format(
'{} subset'.format(len_subsets)
'{} subsets'.format(len_subsets)
)
self.values = values
@ -310,23 +316,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
roleList = ['Pypeclub', 'Administrator']
username = session.api_user
user = session.query('User where username is "{}"'.format(username)).one()
available = False
for role in user['user_security_roles']:
if role['security_role']['name'] in roleList:
available = True
break
if available is True:
DeleteAsset(session).register()
else:
logging.info(
"!!! You're missing required permissions for action {}".format(
DeleteAsset.__name__
)
)
DeleteAsset(session).register()
def main(arguments=None):

View file

@ -15,6 +15,13 @@ class AssetsRemover(BaseAction):
label = 'Delete Assets by Name'
#: Action description.
description = 'Removes assets from Ftrack and Avalon db with all childs'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
icon = (
'https://cdn4.iconfinder.com/data/icons/'
'ios-web-user-interface-multi-circle-flat-vol-5/512/'
'Clipboard_copy_delete_minus_paste_remove-512.png'
)
#: Db
db = DbConnector()
@ -135,23 +142,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
roleList = ['Pypeclub', 'Administrator']
username = session.api_user
user = session.query('User where username is "{}"'.format(username)).one()
available = False
for role in user['user_security_roles']:
if role['security_role']['name'] in roleList:
available = True
break
if available is True:
AssetsRemover(session).register()
else:
logging.info(
"!!! You're missing required permissions for action {}".format(
AssetsRemover.__name__
)
)
AssetsRemover(session).register()
def main(arguments=None):

View file

@ -51,8 +51,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
action_handler = VersionsCleanup(session)
action_handler.register()
VersionsCleanup(session).register()
def main(arguments=None):

View file

@ -17,7 +17,12 @@ class JobKiller(BaseAction):
label = 'Job Killer'
#: Action description.
description = 'Killing all running jobs younger than day'
#: roles that are allowed to register this action
role_list = ['Pypeclub', 'Administrator']
icon = (
'https://cdn2.iconfinder.com/data/icons/new-year-resolutions/64/'
'resolutions-23-512.png'
)
def prediscover(self, event):
''' Validation '''
@ -103,23 +108,8 @@ def register(session, **kw):
# return without doing anything.
if not isinstance(session, ftrack_api.session.Session):
return
roleList = ['Pypeclub', 'Administrator']
username = session.api_user
user = session.query('User where username is "{}"'.format(username)).one()
available = False
for role in user['user_security_roles']:
if role['security_role']['name'] in roleList:
available = True
break
if available is True:
JobKiller(session).register()
else:
logging.info(
"!!! You're missing required permissions for action {}".format(
JobKiller.__name__
)
)
JobKiller(session).register()
def main(arguments=None):

View file

@ -80,8 +80,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
action_handler = SetVersion(session)
action_handler.register()
SetVersion(session).register()
def main(arguments=None):

View file

@ -54,6 +54,8 @@ class SyncToAvalon(BaseAction):
'https://cdn1.iconfinder.com/data/icons/hawcons/32/'
'699650-icon-92-inbox-download-512.png'
)
#: roles that are allowed to register this action
role_list = ['Pypeclub']
#: Action priority
priority = 200
@ -223,23 +225,8 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
roleList = ['Pypeclub']
SyncToAvalon(session).register()
username = session.api_user
user = session.query('User where username is "{}"'.format(username)).one()
available = False
for role in user['user_security_roles']:
if role['security_role']['name'] in roleList:
available = True
break
if available is True:
SyncToAvalon(session).register()
else:
logging.info(
"!!! You're missing required permissions for action {}".format(
SyncToAvalon.__name__
)
)
def main(arguments=None):
'''Set up logging and register action.'''

View file

@ -25,8 +25,13 @@ class TestAction(BaseAction):
description = 'Test action'
#: priority
priority = 10000
def prediscover(self, session, entities, event):
#: roles that are allowed to register this action
role_list = ['Pypecub']
icon = (
'https://cdn4.iconfinder.com/data/icons/hospital-19/512/'
'8_hospital-512.png'
)
def prediscover(self, event):
''' Validation '''
return True
@ -43,23 +48,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
roleList = ['Pypeclub']
username = session.api_user
user = session.query('User where username is "{}"'.format(username)).one()
available = False
for role in user['user_security_roles']:
if role['security_role']['name'] in roleList:
available = True
break
if available is True:
TestAction(session).register()
else:
logging.info(
"!!! You're missing required permissions for action {}".format(
TestAction.__name__
)
)
TestAction(session).register()
def main(arguments=None):

View file

@ -69,8 +69,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
action_handler = ThumbToChildren(session)
action_handler.register()
ThumbToChildren(session).register()
def main(arguments=None):

View file

@ -91,8 +91,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
action_handler = ThumbToParent(session)
action_handler.register()
ThumbToParent(session).register()
def main(arguments=None):

View file

@ -0,0 +1,70 @@
import ftrack_api
from pype.ftrack import BaseEvent
class CollectEntities(BaseEvent):
priority = 1
def _launch(self, event):
entities, entity_types = self.translate_event(event)
entities_count = len(entities)
event['data']['entities'] = entities
event['data']['entity_types'] = entity_types
event['data']['entities_count'] = entities_count
return True
def translate_event(self, event):
selection = event['data'].get('selection', [])
entities = list()
entity_types = set()
for entity in selection:
ent = self.session.get(
self.get_entity_type(entity),
entity.get('entityId')
)
entities.append(ent)
entity_types.add(ent.entity_type)
return [entities, entity_types]
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 register(self):
self.session.event_hub.subscribe(
'topic=ftrack.action.discover'
' and source.user.username={0}'.format(self.session.api_user),
self._launch,
priority=self.priority
)
def register(session, **kw):
'''Register plugin. Called when used as an plugin.'''
if not isinstance(session, ftrack_api.session.Session):
return
CollectEntities(session).register()

View file

@ -1,54 +1,65 @@
import os
import toml
import json
import ftrack_api
import appdirs
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)
if not os.path.isdir(folder):
os.makedirs(folder)
def _get_credentials():
folder = os.path.dirname(fpath)
action_file_name = 'ftrack_cred.json'
event_file_name = 'ftrack_event_cred.json'
action_fpath = os.path.join(config_path, action_file_name)
event_fpath = os.path.join(config_path, event_file_name)
folders = set([os.path.dirname(action_fpath), os.path.dirname(event_fpath)])
for folder in folders:
if not os.path.isdir(folder):
os.makedirs(folder)
def _get_credentials(event=False):
if event:
fpath = event_fpath
else:
fpath = action_fpath
credentials = {}
try:
file = open(fpath, 'r')
credentials = json.load(file)
except Exception:
filecreate = open(fpath, 'w')
filecreate.close()
file = open(fpath, 'r')
file = open(fpath, 'w')
credentials = toml.load(file)
file.close()
return credentials
def _save_credentials(username, apiKey):
file = open(fpath, 'w')
def _save_credentials(username, apiKey, event=False, auto_connect=None):
data = {
'username': username,
'apiKey': apiKey
}
credentials = toml.dumps(data)
file.write(credentials)
if event:
fpath = event_fpath
if auto_connect is None:
cred = _get_credentials(True)
auto_connect = cred.get('auto_connect', False)
data['auto_connect'] = auto_connect
else:
fpath = action_fpath
file = open(fpath, 'w')
file.write(json.dumps(data))
file.close()
def _clear_credentials():
file = open(fpath, 'w').close()
def _clear_credentials(event=False):
if event:
fpath = event_fpath
else:
fpath = action_fpath
open(fpath, 'w').close()
_set_env(None, None)

View file

@ -240,8 +240,7 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
action_handler = Sync_To_Avalon(session)
action_handler.register()
Sync_To_Avalon(session).register()
def main(arguments=None):

View file

@ -83,5 +83,4 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
event = NextTaskUpdate(session)
event.register()
NextTaskUpdate(session).register()

View file

@ -165,5 +165,4 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
event = Sync_to_Avalon(session)
event.register()
Sync_to_Avalon(session).register()

View file

@ -27,5 +27,4 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
event = Test_Event(session)
event.register()
Test_Event(session).register()

View file

@ -45,5 +45,4 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
event = ThumbnailEvents(session)
event.register()
ThumbnailEvents(session).register()

View file

@ -75,5 +75,4 @@ def register(session, **kw):
if not isinstance(session, ftrack_api.session.Session):
return
event = VersionToTaskStatus(session)
event.register()
VersionToTaskStatus(session).register()

View file

@ -9,7 +9,7 @@ from app.vendor.Qt import QtCore, QtGui, QtWidgets
from pype.ftrack import credentials, login_dialog as login_dialog
from pype.vendor.pynput import mouse, keyboard
from FtrackServer import FtrackServer
from . import FtrackServer
from pype import api as pype

View file

@ -0,0 +1,8 @@
from .ftrack_server import FtrackServer
from . import event_server, event_server_cli
__all__ = [
'event_server',
'event_server_cli',
'FtrackServer'
]

View file

@ -1,6 +1,6 @@
import sys
from pype.ftrack import credentials, login_dialog as login_dialog
from FtrackServer import FtrackServer
from pype.ftrack.ftrack_server import FtrackServer
from app.vendor.Qt import QtWidgets
from pype import api
@ -9,10 +9,12 @@ log = api.Logger.getLogger(__name__, "ftrack-event-server")
class EventServer:
def __init__(self):
self.login_widget = login_dialog.Login_Dialog_ui(self)
self.login_widget = login_dialog.Login_Dialog_ui(
parent=self, is_event=True
)
self.event_server = FtrackServer('event')
cred = credentials._get_credentials()
cred = credentials._get_credentials(True)
if 'username' in cred and 'apiKey' in cred:
self.login_widget.user_input.setText(cred['username'])
@ -24,6 +26,7 @@ class EventServer:
def loginChange(self):
log.info("Logged successfully")
self.login_widget.close()
self.event_server.run_server()

View file

@ -0,0 +1,114 @@
import sys
from pype.ftrack import credentials
from pype.ftrack.ftrack_server import FtrackServer
from app import api
log = api.Logger.getLogger(__name__, "ftrack-event-server-cli")
possible_yes = ['y', 'yes']
possible_no = ['n', 'no']
possible_third = ['a', 'auto']
possible_exit = ['exit']
def ask_yes_no(third=False):
msg = "Y/N:"
if third:
msg = "Y/N/AUTO:"
log.info(msg)
response = input().lower()
if response in possible_exit:
sys.exit()
elif response in possible_yes:
return True
elif response in possible_no:
return False
else:
all_entries = possible_no
all_entries.extend(possible_yes)
if third is True:
if response in possible_third:
return 'auto'
else:
all_entries.extend(possible_third)
all_entries.extend(possible_exit)
all_entries = ', '.join(all_entries)
log.info(
'Invalid input. Possible entries: [{}]. Try it again:'.foramt(
all_entries
)
)
return ask_yes_no()
def cli_login():
enter_cred = True
cred_data = credentials._get_credentials(True)
user = cred_data.get('username', None)
key = cred_data.get('apiKey', None)
auto = cred_data.get('auto_connect', False)
if user is None or key is None:
log.info(
'Credentials are not set. Do you want to enter them now? (Y/N)'
)
if ask_yes_no() is False:
log.info("Exiting...")
return
elif credentials._check_credentials(user, key):
if auto is False:
log.info((
'Do you want to log with username {}'
' enter "auto" if want to autoconnect next time (Y/N/AUTO)'
).format(
user
))
result = ask_yes_no(True)
if result is True:
enter_cred = False
elif result == 'auto':
credentials._save_credentials(user, key, True, True)
enter_cred = False
else:
enter_cred = False
else:
log.info(
'Stored credentials are not valid.'
' Do you want enter them now?(Y/N)'
)
if ask_yes_no() is False:
log.info("Exiting...")
return
while enter_cred:
log.info('Please enter Ftrack API User:')
user = input()
log.info('And now enter Ftrack API Key:')
key = input()
if credentials._check_credentials(user, key):
log.info(
'Credentials are valid.'
' Do you want to auto-connect next time?(Y/N)'
)
credentials._save_credentials(user, key, True, ask_yes_no())
enter_cred = False
break
else:
log.info(
'Entered credentials are not valid.'
' Do you want to try it again?(Y/N)'
)
if ask_yes_no() is False:
log.info('Exiting...')
return
server = FtrackServer('event')
server.run_server()
def main():
cli_login()
if (__name__ == ('__main__')):
main()

View file

@ -0,0 +1,157 @@
import os
import sys
import types
import importlib
import ftrack_api
import time
import logging
from app.api import Logger
log = Logger.getLogger(__name__)
"""
# Required - Needed for connection to Ftrack
FTRACK_SERVER # Ftrack server e.g. "https://myFtrack.ftrackapp.com"
FTRACK_API_KEY # Ftrack user's API key "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
FTRACK_API_USER # Ftrack username e.g. "user.name"
# Required - Paths to folder with actions
FTRACK_ACTIONS_PATH # Paths to folders where are located actions
- EXAMPLE: "M:/FtrackApi/../actions/"
FTRACK_EVENTS_PATH # Paths to folders where are located actions
- EXAMPLE: "M:/FtrackApi/../events/"
# Required - Needed for import included modules
PYTHONPATH # Path to ftrack_api and paths to all modules used in actions
- path to ftrack_action_handler, etc.
"""
class FtrackServer():
def __init__(self, type='action'):
"""
- 'type' is by default set to 'action' - Runs Action server
- enter 'event' for Event server
EXAMPLE FOR EVENT SERVER:
...
server = FtrackServer('event')
server.run_server()
..
"""
# set Ftrack logging to Warning only - OPTIONAL
ftrack_log = logging.getLogger("ftrack_api")
ftrack_log.setLevel(logging.WARNING)
self.type = type
self.actionsAvailable = True
self.eventsAvailable = True
# Separate all paths
if "FTRACK_ACTIONS_PATH" in os.environ:
all_action_paths = os.environ["FTRACK_ACTIONS_PATH"]
self.actionsPaths = all_action_paths.split(os.pathsep)
else:
self.actionsAvailable = False
if "FTRACK_EVENTS_PATH" in os.environ:
all_event_paths = os.environ["FTRACK_EVENTS_PATH"]
self.eventsPaths = all_event_paths.split(os.pathsep)
else:
self.eventsAvailable = False
def stop_session(self):
if self.session.event_hub.connected is True:
self.session.event_hub.disconnect()
self.session.close()
self.session = None
def set_files(self, paths):
# Iterate all paths
functions = []
for path in paths:
# add path to PYTHON PATH
if path not in sys.path:
sys.path.append(path)
# Get all modules with functions
for file in os.listdir(path):
# Get only .py files with action functions
try:
if '.pyc' in file or '.py' not in file:
continue
ignore = 'ignore_me'
mod = importlib.import_module(os.path.splitext(file)[0])
importlib.reload(mod)
mod_functions = dict(
[
(name, function)
for name, function in mod.__dict__.items()
if isinstance(function, types.FunctionType) or
name == ignore
]
)
# Don't care about ignore_me files
if (
ignore in mod_functions and
mod_functions[ignore] is True
):
continue
# separate files by register function
if 'register' not in mod_functions:
msg = (
'"{0}" - Missing register method'
).format(file, self.type)
log.warning(msg)
continue
functions.append({
'name': file,
'register': mod_functions['register']
})
except Exception as e:
msg = 'Loading of file "{}" failed ({})'.format(
file, str(e)
)
log.warning(msg)
if len(functions) < 1:
raise Exception
for function in functions:
try:
function['register'](self.session)
except Exception as e:
msg = '"{}" - register was not successful ({})'.format(
function['name'], str(e)
)
log.warning(msg)
time.sleep(0.05)
def run_server(self):
self.session = ftrack_api.Session(auto_connect_event_hub=True,)
if self.type.lower() == 'event':
if self.eventsAvailable is False:
msg = (
'FTRACK_EVENTS_PATH is not set'
', event server won\'t launch'
)
log.error(msg)
return
self.set_files(self.eventsPaths)
else:
if self.actionsAvailable is False:
msg = (
'FTRACK_ACTIONS_PATH is not set'
', action server won\'t launch'
)
log.error(msg)
return
self.set_files(self.actionsPaths)
log.info(60*"*")
log.info('Registration of actions/events has finished!')
# keep event_hub on session running
self.session.event_hub.wait()

View file

@ -143,10 +143,6 @@ class AppAction(BaseHandler):
'''
self.log.info((
"Action - {0} ({1}) - just started"
).format(self.label, self.identifier))
entity = entities[0]
project_name = entity['project']['full_name']

View file

@ -4,6 +4,11 @@ import time
from pype import api as pype
class MissingPermision(Exception):
def __init__(self):
super().__init__('Missing permission')
class BaseHandler(object):
'''Custom Action base class
@ -25,10 +30,11 @@ class BaseHandler(object):
self.log = pype.Logger.getLogger(self.__class__.__name__)
# Using decorator
self.register = self.register_log(self.register)
self.register = self.register_decorator(self.register)
self.launch = self.launch_log(self.launch)
# Decorator
def register_log(self, func):
def register_decorator(self, func):
@functools.wraps(func)
def wrapper_register(*args, **kwargs):
label = self.__class__.__name__
@ -37,8 +43,20 @@ class BaseHandler(object):
label = self.label
else:
label = '{} {}'.format(self.label, self.variant)
try:
if hasattr(self, "role_list") and len(self.role_list) > 0:
username = self.session.api_user
user = self.session.query(
'User where username is "{}"'.format(username)
).one()
available = False
for role in user['user_security_roles']:
if role['security_role']['name'] in self.role_list:
available = True
break
if available is False:
raise MissingPermision
start_time = time.perf_counter()
func(*args, **kwargs)
end_time = time.perf_counter()
@ -46,6 +64,10 @@ class BaseHandler(object):
self.log.info((
'{} "{}" - Registered successfully ({:.4f}sec)'
).format(self.type, label, run_time))
except MissingPermision:
self.log.info((
'!{} "{}" - You\'re missing required permissions'
).format(self.type, label))
except NotImplementedError:
self.log.error((
'{} "{}" - Register method is not implemented'
@ -58,6 +80,29 @@ class BaseHandler(object):
)
return wrapper_register
# Decorator
def launch_log(self, func):
@functools.wraps(func)
def wrapper_launch(*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:
result = func(*args, **kwargs)
self.log.info((
'{} "{}" Launched'
).format(self.type, label))
return result
except Exception as e:
self.log.error('{} "{}": Launch failed ({})'.format(
self.type, label, str(e))
)
return wrapper_launch
@property
def session(self):
'''Return current session.'''
@ -126,19 +171,7 @@ class BaseHandler(object):
'''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')
)
)
)
_entities = event['data']['entities']
return [
_entities,
event

View file

@ -16,11 +16,12 @@ class Login_Dialog_ui(QtWidgets.QWidget):
buttons = []
labels = []
def __init__(self, parent=None):
def __init__(self, parent=None, is_event=False):
super(Login_Dialog_ui, self).__init__()
self.parent = parent
self.is_event = is_event
if hasattr(parent, 'icon'):
self.setWindowIcon(self.parent.icon)
@ -205,7 +206,7 @@ class Login_Dialog_ui(QtWidgets.QWidget):
verification = credentials._check_credentials(username, apiKey)
if verification:
credentials._save_credentials(username, apiKey)
credentials._save_credentials(username, apiKey, self.is_event)
credentials._set_env(username, apiKey)
if self.parent is not None:
self.parent.loginChange()
@ -305,7 +306,7 @@ class Login_Dialog_ui(QtWidgets.QWidget):
verification = credentials._check_credentials(username, apiKey)
if verification is True:
credentials._save_credentials(username, apiKey)
credentials._save_credentials(username, apiKey, self.is_event)
credentials._set_env(username, apiKey)
if self.parent is not None:
self.parent.loginChange()