mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
commit
94d8c84d88
25 changed files with 3178 additions and 363 deletions
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
131
pype/ftrack/actions/action_asset_delete.py
Normal file
131
pype/ftrack/actions/action_asset_delete.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import getpass
|
||||||
|
import ftrack_api
|
||||||
|
from ftrack_action_handler import BaseAction
|
||||||
|
|
||||||
|
|
||||||
|
class AssetDelete(BaseAction):
|
||||||
|
'''Custom action.'''
|
||||||
|
|
||||||
|
#: Action identifier.
|
||||||
|
identifier = 'asset.delete'
|
||||||
|
#: 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']):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def interface(self, session, entities, event):
|
||||||
|
|
||||||
|
if not event['data'].get('values', {}):
|
||||||
|
entity = entities[0]
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for asset in entity['assets']:
|
||||||
|
# get asset name for label
|
||||||
|
label = 'None'
|
||||||
|
if asset['name']:
|
||||||
|
label = asset['name']
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
'label':label,
|
||||||
|
'name':label,
|
||||||
|
'value':False,
|
||||||
|
'type':'boolean'
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(items) < 1:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': 'There are no assets to delete'
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def launch(self, session, entities, event):
|
||||||
|
|
||||||
|
entity = entities[0]
|
||||||
|
# if values were set remove those items
|
||||||
|
if 'values' in event['data']:
|
||||||
|
values = event['data']['values']
|
||||||
|
# get list of assets to delete from form
|
||||||
|
to_delete = []
|
||||||
|
for key in values:
|
||||||
|
if values[key]:
|
||||||
|
to_delete.append(key)
|
||||||
|
# delete them by name
|
||||||
|
for asset in entity['assets']:
|
||||||
|
if asset['name'] in to_delete:
|
||||||
|
session.delete(asset)
|
||||||
|
try:
|
||||||
|
session.commit()
|
||||||
|
except:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Asset deleted.'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register(session, **kw):
|
||||||
|
'''Register action. Called when used as an event 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 = AssetDelete(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:]))
|
||||||
103
pype/ftrack/actions/action_client_review_sort.py
Normal file
103
pype/ftrack/actions/action_client_review_sort.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
import ftrack_api
|
||||||
|
from ftrack_action_handler import BaseAction
|
||||||
|
|
||||||
|
|
||||||
|
class ClientReviewSort(BaseAction):
|
||||||
|
'''Custom action.'''
|
||||||
|
|
||||||
|
#: Action identifier.
|
||||||
|
identifier = 'client.review.sort'
|
||||||
|
|
||||||
|
#: Action label.
|
||||||
|
label = 'Sort Review'
|
||||||
|
|
||||||
|
|
||||||
|
def discover(self, session, entities, event):
|
||||||
|
''' Validation '''
|
||||||
|
|
||||||
|
if (len(entities) == 0 or entities[0].entity_type != 'ReviewSession'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def launch(self, session, entities, event):
|
||||||
|
|
||||||
|
entity = entities[0]
|
||||||
|
|
||||||
|
# Get all objects from Review Session and all 'sort order' possibilities
|
||||||
|
obj_list = []
|
||||||
|
sort_order_list = []
|
||||||
|
for obj in entity['review_session_objects']:
|
||||||
|
obj_list.append(obj)
|
||||||
|
sort_order_list.append(obj['sort_order'])
|
||||||
|
|
||||||
|
# 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['name'])
|
||||||
|
|
||||||
|
# Set 'sort order' to sorted list, so they are sorted in Ftrack also
|
||||||
|
for i in range(len(obj_list)):
|
||||||
|
obj_list[i]['sort_order'] = sort_order_list[i]
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Client Review sorted!'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register(session, **kw):
|
||||||
|
'''Register action. Called when used as an event plugin.'''
|
||||||
|
if not isinstance(session, ftrack_api.session.Session):
|
||||||
|
return
|
||||||
|
|
||||||
|
action_handler = ClientReviewSort(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:]))
|
||||||
122
pype/ftrack/actions/action_component_open.py
Normal file
122
pype/ftrack/actions/action_component_open.py
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# :coding: utf-8
|
||||||
|
# :copyright: Copyright (c) 2015 Milan Kolar
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import getpass
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import ftrack_api
|
||||||
|
from ftrack_action_handler import BaseAction
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentOpen(BaseAction):
|
||||||
|
'''Custom action.'''
|
||||||
|
|
||||||
|
# Action identifier
|
||||||
|
identifier = 'component.open'
|
||||||
|
# Action label
|
||||||
|
label = 'Open File'
|
||||||
|
# Action icon
|
||||||
|
icon = 'https://cdn4.iconfinder.com/data/icons/rcons-application/32/application_go_run-256.png',
|
||||||
|
|
||||||
|
|
||||||
|
def discover(self, session, entities, event):
|
||||||
|
''' Validation '''
|
||||||
|
if len(entities) != 1 or entities[0].entity_type != 'FileComponent':
|
||||||
|
return False
|
||||||
|
|
||||||
|
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':
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': "This component is stored on ftrack server!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get component filepath
|
||||||
|
# TODO with locations it will be different???
|
||||||
|
fpath = entity['component_locations'][0]['resource_identifier']
|
||||||
|
items = fpath.split(os.sep)
|
||||||
|
items.pop(-1)
|
||||||
|
fpath = os.sep.join(items)
|
||||||
|
|
||||||
|
if os.path.isdir(fpath):
|
||||||
|
if 'win' in sys.platform: # windows
|
||||||
|
subprocess.Popen('explorer "%s"' % fpath)
|
||||||
|
elif sys.platform == 'darwin': # macOS
|
||||||
|
subprocess.Popen(['open', fpath])
|
||||||
|
else: # linux
|
||||||
|
try:
|
||||||
|
subprocess.Popen(['xdg-open', fpath])
|
||||||
|
except OSError:
|
||||||
|
raise OSError('unsupported xdg-open call??')
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': "Didn't found file: " + fpath
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Component folder Opened'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register(session, **kw):
|
||||||
|
'''Register action. Called when used as an event 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 = ComponentOpen(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:]))
|
||||||
|
|
@ -11,6 +11,7 @@ from ftrack_action_handler import BaseAction
|
||||||
from avalon import io, inventory, lib
|
from avalon import io, inventory, lib
|
||||||
from avalon.vendor import toml
|
from avalon.vendor import toml
|
||||||
|
|
||||||
|
|
||||||
class AvalonIdAttribute(BaseAction):
|
class AvalonIdAttribute(BaseAction):
|
||||||
'''Edit meta data action.'''
|
'''Edit meta data action.'''
|
||||||
|
|
||||||
|
|
@ -21,139 +22,21 @@ class AvalonIdAttribute(BaseAction):
|
||||||
#: Action description.
|
#: Action description.
|
||||||
description = 'Creates Avalon/Mongo ID for double check'
|
description = 'Creates Avalon/Mongo ID for double check'
|
||||||
|
|
||||||
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):
|
def discover(self, session, entities, event):
|
||||||
'''Return True if action is valid.'''
|
''' Validation '''
|
||||||
|
|
||||||
self.logger.info('Got selection: {0}'.format(entities))
|
# userId = event['source']['user']['id']
|
||||||
return self.validate_selection(session, entities)
|
# user = session.query('User where id is ' + userId).one()
|
||||||
|
# if user['user_security_roles'][0]['security_role']['name'] != 'Administrator':
|
||||||
|
# return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
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"]
|
|
||||||
os.environ["AVALON_ASSET"] = entityProj['full_name']
|
|
||||||
|
|
||||||
# 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"}
|
|
||||||
|
|
||||||
# --- Create project and assets in Avalon ---
|
|
||||||
io.install()
|
|
||||||
# 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)
|
|
||||||
else:
|
|
||||||
io.update_many({'type': 'project','name': entityProj['full_name']},
|
|
||||||
{'$set':{'config':config}})
|
|
||||||
|
|
||||||
# Store info about project (FtrackId)
|
|
||||||
io.update_many({'type': 'project','name': entityProj['full_name']},
|
|
||||||
{'$set':{'data':{'ftrackId':entityProj['id'],'entityType':entityProj.entity_type}}})
|
|
||||||
|
|
||||||
# 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):
|
def launch(self, session, entities, event):
|
||||||
# JOB SETTINGS
|
# JOB SETTINGS
|
||||||
|
|
||||||
userId = event['source']['user']['id']
|
userId = event['source']['user']['id']
|
||||||
user = session.query('User where id is ' + userId).one()
|
user = session.query('User where id is ' + userId).one()
|
||||||
|
|
||||||
|
|
@ -161,44 +44,90 @@ class AvalonIdAttribute(BaseAction):
|
||||||
'user': user,
|
'user': user,
|
||||||
'status': 'running',
|
'status': 'running',
|
||||||
'data': json.dumps({
|
'data': json.dumps({
|
||||||
'description': 'Synch Ftrack to Avalon.'
|
'description': 'Custom Attribute creation.'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
session.commit()
|
||||||
try:
|
try:
|
||||||
print("action <" + self.__class__.__name__ + "> is running")
|
# Attribute Name and Label
|
||||||
#TODO It's better to have these env set, are they used anywhere?
|
custAttrName = 'avalon_mongo_id'
|
||||||
os.environ['AVALON_PROJECTS'] = "tmp"
|
custAttrLabel = 'Avalon/Mongo Id'
|
||||||
os.environ['AVALON_ASSET'] = "tmp"
|
# Types that don't need object_type_id
|
||||||
os.environ['AVALON_SILO'] = "tmp"
|
base = {'show','asset','assetversion'}
|
||||||
importable = []
|
# Don't create custom attribute on these entity types:
|
||||||
|
exceptions = ['task','milestone','library']
|
||||||
|
exceptions.extend(base)
|
||||||
|
# Get all possible object types
|
||||||
|
all_obj_types = session.query('ObjectType').all()
|
||||||
|
count_types = len(all_obj_types)
|
||||||
|
# Filter object types by exceptions
|
||||||
|
for index in range(count_types):
|
||||||
|
i = count_types - 1 - index
|
||||||
|
name = all_obj_types[i]['name'].lower()
|
||||||
|
|
||||||
def getShotAsset(entity):
|
if " " in name:
|
||||||
if not (entity.entity_type in ['Task']):
|
name = name.replace(" ","")
|
||||||
if entity not in importable:
|
|
||||||
importable.append(entity)
|
|
||||||
|
|
||||||
if entity['children']:
|
if name in exceptions:
|
||||||
childrens = entity['children']
|
all_obj_types.pop(i)
|
||||||
for child in childrens:
|
|
||||||
getShotAsset(child)
|
|
||||||
|
|
||||||
# get all entities separately
|
# Get IDs of filtered object types
|
||||||
for entity in entities:
|
all_obj_types_id = set()
|
||||||
entity_type, entity_id = entity
|
for obj in all_obj_types:
|
||||||
act_ent = session.get(entity_type, entity_id)
|
all_obj_types_id.add(obj['id'])
|
||||||
getShotAsset(act_ent)
|
|
||||||
|
|
||||||
for e in importable:
|
# Get all custom attributes
|
||||||
self.importToAvalon(session, e)
|
current_cust_attr = session.query('CustomAttributeConfiguration').all()
|
||||||
|
# Filter already existing AvalonMongoID attr.
|
||||||
|
for attr in current_cust_attr:
|
||||||
|
if attr['key'] == custAttrName:
|
||||||
|
if attr['entity_type'] in base:
|
||||||
|
base.remove(attr['entity_type'])
|
||||||
|
if attr['object_type_id'] in all_obj_types_id:
|
||||||
|
all_obj_types_id.remove(attr['object_type_id'])
|
||||||
|
|
||||||
|
# Set session back to begin("session.query" raises error on commit)
|
||||||
|
session.rollback()
|
||||||
|
# Set security roles for attribute
|
||||||
|
custAttrSecuRole = session.query('SecurityRole').all()
|
||||||
|
# Set Text type of Attribute
|
||||||
|
custom_attribute_type = session.query(
|
||||||
|
'CustomAttributeType where name is "text"'
|
||||||
|
).one()
|
||||||
|
|
||||||
|
for entity_type in base:
|
||||||
|
# Create a custom attribute configuration.
|
||||||
|
session.create('CustomAttributeConfiguration', {
|
||||||
|
'entity_type': entity_type,
|
||||||
|
'type': custom_attribute_type,
|
||||||
|
'label': custAttrLabel,
|
||||||
|
'key': custAttrName,
|
||||||
|
'default': '',
|
||||||
|
'write_security_roles': custAttrSecuRole,
|
||||||
|
'read_security_roles': custAttrSecuRole,
|
||||||
|
'config': json.dumps({'markdown': False})
|
||||||
|
})
|
||||||
|
|
||||||
|
for type in all_obj_types_id:
|
||||||
|
# Create a custom attribute configuration.
|
||||||
|
session.create('CustomAttributeConfiguration', {
|
||||||
|
'entity_type': 'task',
|
||||||
|
'object_type_id': type,
|
||||||
|
'type': custom_attribute_type,
|
||||||
|
'label': custAttrLabel,
|
||||||
|
'key': custAttrName,
|
||||||
|
'default': '',
|
||||||
|
'write_security_roles': custAttrSecuRole,
|
||||||
|
'read_security_roles': custAttrSecuRole,
|
||||||
|
'config': json.dumps({'markdown': False})
|
||||||
|
})
|
||||||
|
|
||||||
job['status'] = 'done'
|
job['status'] = 'done'
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
print('Synchronization to Avalon was successfull!')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
job['status'] = 'failed'
|
job['status'] = 'failed'
|
||||||
print('During synchronization to Avalon went something wrong!')
|
print("Creating custom attributes failed")
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
@ -215,7 +144,6 @@ def register(session, **kw):
|
||||||
|
|
||||||
action_handler = AvalonIdAttribute(session)
|
action_handler = AvalonIdAttribute(session)
|
||||||
action_handler.register()
|
action_handler.register()
|
||||||
print("----- action - <" + action_handler.__class__.__name__ + "> - Has been registered -----")
|
|
||||||
|
|
||||||
|
|
||||||
def main(arguments=None):
|
def main(arguments=None):
|
||||||
|
|
|
||||||
96
pype/ftrack/actions/action_delete_unpublished.py
Normal file
96
pype/ftrack/actions/action_delete_unpublished.py
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import getpass
|
||||||
|
import ftrack_api
|
||||||
|
from ftrack_action_handler import BaseAction
|
||||||
|
|
||||||
|
|
||||||
|
class VersionsCleanup(BaseAction):
|
||||||
|
'''Custom action.'''
|
||||||
|
|
||||||
|
# Action identifier
|
||||||
|
identifier = 'versions.cleanup'
|
||||||
|
# Action label
|
||||||
|
label = 'Versions cleanup'
|
||||||
|
|
||||||
|
|
||||||
|
def discover(self, session, entities, event):
|
||||||
|
''' Validation '''
|
||||||
|
|
||||||
|
# Only 1 AssetVersion is allowed
|
||||||
|
if len(entities) != 1 or entities[0].entity_type != 'AssetVersion':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def launch(self, session, entities, event):
|
||||||
|
|
||||||
|
entity = entities[0]
|
||||||
|
|
||||||
|
# Go through all versions in asset
|
||||||
|
for version in entity['asset']['versions']:
|
||||||
|
if not version['is_published']:
|
||||||
|
session.delete(version)
|
||||||
|
try:
|
||||||
|
session.commit()
|
||||||
|
except:
|
||||||
|
session.rollback()
|
||||||
|
raise
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'removed hidden versions'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register(session, **kw):
|
||||||
|
'''Register action. Called when used as an event 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 = VersionsCleanup(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:]))
|
||||||
|
|
@ -20,20 +20,14 @@ class JobKiller(BaseAction):
|
||||||
description = 'Killing all running jobs younger than day'
|
description = 'Killing all running jobs younger than day'
|
||||||
|
|
||||||
|
|
||||||
def validate_selection(self, session, entities):
|
def discover(self, session, entities, event):
|
||||||
'''Return if *entities* is a valid selection.'''
|
''' Validation '''
|
||||||
pass
|
|
||||||
|
|
||||||
return True
|
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 launch(self, session, entities, event):
|
def launch(self, session, entities, event):
|
||||||
""" JOB SETTING """
|
""" GET JOB """
|
||||||
|
|
||||||
yesterday = datetime.date.today() - datetime.timedelta(days=1)
|
yesterday = datetime.date.today() - datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
|
@ -48,7 +42,10 @@ class JobKiller(BaseAction):
|
||||||
print('Changing Job ({}) status: {} -> failed'.format(job['id'], job['status']))
|
print('Changing Job ({}) status: {} -> failed'.format(job['id'], job['status']))
|
||||||
job['status'] = 'failed'
|
job['status'] = 'failed'
|
||||||
|
|
||||||
session.commit()
|
try:
|
||||||
|
session.commit()
|
||||||
|
except:
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
print('All running jobs were killed Successfully!')
|
print('All running jobs were killed Successfully!')
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
125
pype/ftrack/actions/action_set_version.py
Normal file
125
pype/ftrack/actions/action_set_version.py
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import getpass
|
||||||
|
import ftrack_api
|
||||||
|
from ftrack_action_handler import BaseAction
|
||||||
|
|
||||||
|
|
||||||
|
class SetVersion(BaseAction):
|
||||||
|
'''Custom action.'''
|
||||||
|
|
||||||
|
#: Action identifier.
|
||||||
|
identifier = 'version.set'
|
||||||
|
|
||||||
|
#: Action label.
|
||||||
|
label = 'Version Set'
|
||||||
|
|
||||||
|
|
||||||
|
def discover(self, session, entities, event):
|
||||||
|
''' Validation '''
|
||||||
|
|
||||||
|
# Only 1 AssetVersion is allowed
|
||||||
|
if len(entities) != 1 or entities[0].entity_type != 'AssetVersion':
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def interface(self, session, entities, event):
|
||||||
|
|
||||||
|
if not event['data'].get('values', {}):
|
||||||
|
entity = entities[0]
|
||||||
|
|
||||||
|
# Get actual version of asset
|
||||||
|
act_ver = entity['version']
|
||||||
|
# Set form
|
||||||
|
items = [{
|
||||||
|
'label': 'Version number',
|
||||||
|
'type': 'number',
|
||||||
|
'name': 'version_number',
|
||||||
|
'value': act_ver
|
||||||
|
}]
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def launch(self, session, entities, event):
|
||||||
|
|
||||||
|
entity = entities[0]
|
||||||
|
|
||||||
|
# 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'])
|
||||||
|
|
||||||
|
if not values['version_number']:
|
||||||
|
scs = False,
|
||||||
|
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
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': scs,
|
||||||
|
'message': msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register(session, **kw):
|
||||||
|
'''Register action. Called when used as an event 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 = SetVersion(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:]))
|
||||||
|
|
@ -4,7 +4,6 @@ import sys
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
import ftrack_api
|
import ftrack_api
|
||||||
from ftrack_action_handler import BaseAction
|
from ftrack_action_handler import BaseAction
|
||||||
|
|
||||||
|
|
@ -23,27 +22,11 @@ class SyncToAvalon(BaseAction):
|
||||||
#: Action icon.
|
#: Action icon.
|
||||||
icon = 'https://cdn1.iconfinder.com/data/icons/hawcons/32/699650-icon-92-inbox-download-512.png'
|
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):
|
def discover(self, session, entities, event):
|
||||||
'''Return True if action is valid.'''
|
''' Validation '''
|
||||||
|
|
||||||
self.logger.info('Got selection: {0}'.format(entities))
|
return True
|
||||||
return self.validate_selection(session, entities)
|
|
||||||
|
|
||||||
|
|
||||||
def importToAvalon(self, session, entity):
|
def importToAvalon(self, session, entity):
|
||||||
|
|
@ -51,6 +34,7 @@ class SyncToAvalon(BaseAction):
|
||||||
custAttrName = 'avalon_mongo_id'
|
custAttrName = 'avalon_mongo_id'
|
||||||
# TODO read from file, which data are in scope???
|
# TODO read from file, which data are in scope???
|
||||||
# get needed info of entity and all parents
|
# get needed info of entity and all parents
|
||||||
|
|
||||||
for e in entity['link']:
|
for e in entity['link']:
|
||||||
tmp = session.get(e['type'], e['id'])
|
tmp = session.get(e['type'], e['id'])
|
||||||
if e['name'].find(" ") == -1:
|
if e['name'].find(" ") == -1:
|
||||||
|
|
@ -81,7 +65,6 @@ class SyncToAvalon(BaseAction):
|
||||||
'schema': 'avalon-core:config-1.0',
|
'schema': 'avalon-core:config-1.0',
|
||||||
'tasks': [{'name': ''}],
|
'tasks': [{'name': ''}],
|
||||||
'apps': apps,
|
'apps': apps,
|
||||||
# TODO redo work!!!
|
|
||||||
'template': {'work': '','publish':''}
|
'template': {'work': '','publish':''}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,6 +73,7 @@ class SyncToAvalon(BaseAction):
|
||||||
|
|
||||||
# --- Create project and assets in Avalon ---
|
# --- Create project and assets in Avalon ---
|
||||||
io.install()
|
io.install()
|
||||||
|
## ----- PROJECT ------
|
||||||
# If project don't exists -> <Create project> ELSE <Update Config>
|
# If project don't exists -> <Create project> ELSE <Update Config>
|
||||||
if (io.find_one({'type': 'project',
|
if (io.find_one({'type': 'project',
|
||||||
'name': entityProj['full_name']}) is None):
|
'name': entityProj['full_name']}) is None):
|
||||||
|
|
@ -110,10 +94,12 @@ class SyncToAvalon(BaseAction):
|
||||||
# If entity is Project or have only 1 entity kill action
|
# If entity is Project or have only 1 entity kill action
|
||||||
if (len(eLinks) > 1) and not (eLinks[-1]['type'] in ['Project']):
|
if (len(eLinks) > 1) and not (eLinks[-1]['type'] in ['Project']):
|
||||||
|
|
||||||
|
## ----- ASSETS ------
|
||||||
|
# Presets:
|
||||||
# TODO how to check if entity is Asset Library or AssetBuild?
|
# TODO how to check if entity is Asset Library or AssetBuild?
|
||||||
silo = 'Assets' if eLinks[-1]['type'] in ['AssetBuild', 'Library'] else 'Film'
|
silo = 'Assets' if eLinks[-1]['type'] in ['AssetBuild', 'Library'] else 'Film'
|
||||||
os.environ['AVALON_SILO'] = silo
|
os.environ['AVALON_SILO'] = silo
|
||||||
# Create Assets
|
# Get list of assets without project
|
||||||
assets = []
|
assets = []
|
||||||
for i in range(1, len(eLinks)):
|
for i in range(1, len(eLinks)):
|
||||||
assets.append(eLinks[i])
|
assets.append(eLinks[i])
|
||||||
|
|
@ -134,21 +120,30 @@ class SyncToAvalon(BaseAction):
|
||||||
tasks.append(child['name'])
|
tasks.append(child['name'])
|
||||||
data.update({'tasks': tasks})
|
data.update({'tasks': tasks})
|
||||||
|
|
||||||
if (io.find_one({'type': 'asset', 'name': asset['name']}) is None):
|
# Try to find asset in current database
|
||||||
# Create asset in DB
|
avalon_asset = io.find_one({'type': 'asset', 'name': asset['name']})
|
||||||
|
# Create if don't exists
|
||||||
|
if avalon_asset is None:
|
||||||
inventory.create_asset(asset['name'], silo, data, projectId)
|
inventory.create_asset(asset['name'], silo, data, projectId)
|
||||||
print("Asset "+asset['name']+" - created")
|
print("Asset "+asset['name']+" - created")
|
||||||
|
# Raise error if it seems to be different ent. with same name
|
||||||
|
elif (avalon_asset['data']['ftrackId'] != data['ftrackId'] or
|
||||||
|
avalon_asset['data']['visualParent'] != data['visualParent'] or
|
||||||
|
avalon_asset['data']['parents'] != data['parents']):
|
||||||
|
raise ValueError('Possibility of entity name duplication: {}'.format(asset['name']))
|
||||||
|
# Else update info
|
||||||
else:
|
else:
|
||||||
io.update_many({'type': 'asset','name': asset['name']},
|
io.update_many({'type': 'asset','name': asset['name']},
|
||||||
{'$set':{'data':data}})
|
{'$set':{'data':data, 'silo': silo}})
|
||||||
# TODO check if is asset in same folder!!! ???? FEATURE FOR FUTURE
|
# TODO check if is asset in same folder!!! ???? FEATURE FOR FUTURE
|
||||||
print("Asset "+asset["name"]+" - already exist")
|
print("Asset "+asset["name"]+" - updated")
|
||||||
|
|
||||||
|
# Get parent ID and store it to data
|
||||||
parentId = io.find_one({'type': 'asset', 'name': asset['name']})['_id']
|
parentId = io.find_one({'type': 'asset', 'name': asset['name']})['_id']
|
||||||
data.update({'visualParent': parentId, 'parents': folderStruct})
|
data.update({'visualParent': parentId, 'parents': folderStruct})
|
||||||
folderStruct.append(asset['name'])
|
folderStruct.append(asset['name'])
|
||||||
|
|
||||||
|
## 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)
|
# Set custom attribute to avalon/mongo id of entity (parentID is last)
|
||||||
if custAttrName in entity['custom_attributes'] and entity['custom_attributes'][custAttrName] is '':
|
if custAttrName in entity['custom_attributes'] and entity['custom_attributes'][custAttrName] is '':
|
||||||
entity['custom_attributes'][custAttrName] = str(parentId)
|
entity['custom_attributes'][custAttrName] = str(parentId)
|
||||||
|
|
@ -156,6 +151,8 @@ class SyncToAvalon(BaseAction):
|
||||||
io.uninstall()
|
io.uninstall()
|
||||||
|
|
||||||
def launch(self, session, entities, event):
|
def launch(self, session, entities, event):
|
||||||
|
message = ""
|
||||||
|
|
||||||
# JOB SETTINGS
|
# JOB SETTINGS
|
||||||
userId = event['source']['user']['id']
|
userId = event['source']['user']['id']
|
||||||
user = session.query('User where id is ' + userId).one()
|
user = session.query('User where id is ' + userId).one()
|
||||||
|
|
@ -163,17 +160,12 @@ class SyncToAvalon(BaseAction):
|
||||||
job = session.create('Job', {
|
job = session.create('Job', {
|
||||||
'user': user,
|
'user': user,
|
||||||
'status': 'running',
|
'status': 'running',
|
||||||
'data': json.dumps({
|
'data': {'description': 'Synch Ftrack to Avalon.'}
|
||||||
'description': 'Synch Ftrack to Avalon.'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("action <" + self.__class__.__name__ + "> is running")
|
print("action <" + self.__class__.__name__ + "> is running")
|
||||||
#TODO It's better to have these env set, are they used anywhere?
|
#TODO AVALON_PROJECTS, AVALON_ASSET, AVALON_SILO should be set up otherwise console log shows avalon debug
|
||||||
os.environ['AVALON_PROJECTS'] = "tmp"
|
|
||||||
os.environ['AVALON_ASSET'] = "tmp"
|
|
||||||
os.environ['AVALON_SILO'] = "tmp"
|
|
||||||
importable = []
|
importable = []
|
||||||
|
|
||||||
def getShotAsset(entity):
|
def getShotAsset(entity):
|
||||||
|
|
@ -186,25 +178,33 @@ class SyncToAvalon(BaseAction):
|
||||||
for child in childrens:
|
for child in childrens:
|
||||||
getShotAsset(child)
|
getShotAsset(child)
|
||||||
|
|
||||||
# get all entities separately
|
# get all entities separately/unique
|
||||||
for entity in entities:
|
for entity in entities:
|
||||||
entity_type, entity_id = entity
|
getShotAsset(entity)
|
||||||
act_ent = session.get(entity_type, entity_id)
|
|
||||||
getShotAsset(act_ent)
|
|
||||||
|
|
||||||
for e in importable:
|
for e in importable:
|
||||||
self.importToAvalon(session, e)
|
self.importToAvalon(session, e)
|
||||||
|
|
||||||
job['status'] = 'done'
|
job['status'] = 'done'
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
print('Synchronization to Avalon was successfull!')
|
print('Synchronization to Avalon was successfull!')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
job['status'] = 'failed'
|
job['status'] = 'failed'
|
||||||
print('During synchronization to Avalon went something wrong!')
|
print('During synchronization to Avalon went something wrong!')
|
||||||
print(e)
|
print(e)
|
||||||
|
message = str(e)
|
||||||
|
|
||||||
return True
|
if len(message) > 0:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': message
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': "Synchronization was successfull"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def register(session, **kw):
|
def register(session, **kw):
|
||||||
|
|
@ -218,7 +218,6 @@ def register(session, **kw):
|
||||||
|
|
||||||
action_handler = SyncToAvalon(session)
|
action_handler = SyncToAvalon(session)
|
||||||
action_handler.register()
|
action_handler.register()
|
||||||
print("----- action - <" + action_handler.__class__.__name__ + "> - Has been registered -----")
|
|
||||||
|
|
||||||
|
|
||||||
def main(arguments=None):
|
def main(arguments=None):
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,38 @@ class TestAction(BaseAction):
|
||||||
|
|
||||||
#: Action identifier.
|
#: Action identifier.
|
||||||
identifier = 'test.action'
|
identifier = 'test.action'
|
||||||
|
|
||||||
#: Action label.
|
#: Action label.
|
||||||
label = 'Test action'
|
label = 'Test action'
|
||||||
|
|
||||||
#: Action description.
|
#: Action description.
|
||||||
description = 'Test action'
|
description = 'Test action'
|
||||||
|
|
||||||
def validate_selection(self, session, entities):
|
|
||||||
'''Return if *entities* is a valid selection.'''
|
|
||||||
pass
|
|
||||||
return True
|
|
||||||
|
|
||||||
def discover(self, session, entities, event):
|
def discover(self, session, entities, event):
|
||||||
'''Return True if action is valid.'''
|
''' Validation '''
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
self.logger.info('Got selection: {0}'.format(entities))
|
|
||||||
return self.validate_selection(session, entities)
|
|
||||||
|
|
||||||
def launch(self, session, entities, event):
|
def launch(self, session, entities, event):
|
||||||
|
|
||||||
|
for entity in entities:
|
||||||
|
index = 0
|
||||||
|
name = entity['components'][index]['name']
|
||||||
|
filetype = entity['components'][index]['file_type']
|
||||||
|
path = entity['components'][index]['component_locations'][0]['resource_identifier']
|
||||||
|
|
||||||
|
# entity['components'][index]['component_locations'][0]['resource_identifier'] = r"C:\Users\jakub.trllo\Desktop\test\exr\int_c022_lighting_v001_main_AO.%04d.exr"
|
||||||
|
location = entity['components'][0]['component_locations'][0]['location']
|
||||||
|
component = entity['components'][0]
|
||||||
|
|
||||||
|
|
||||||
|
# print(location.get_filesystem_path(component))
|
||||||
|
|
||||||
|
# for k in p:
|
||||||
|
# print(100*"-")
|
||||||
|
# print(k)
|
||||||
|
# print(p[k])
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
113
pype/ftrack/actions/action_thumbToChildern.py
Normal file
113
pype/ftrack/actions/action_thumbToChildern.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
# :coding: utf-8
|
||||||
|
# :copyright: Copyright (c) 2015 Milan Kolar
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import getpass
|
||||||
|
import json
|
||||||
|
|
||||||
|
import ftrack_api
|
||||||
|
from ftrack_action_handler import BaseAction
|
||||||
|
|
||||||
|
class ThumbToChildren(BaseAction):
|
||||||
|
'''Custom action.'''
|
||||||
|
|
||||||
|
# Action identifier
|
||||||
|
identifier = 'thumb.to.children'
|
||||||
|
# Action label
|
||||||
|
label = 'Thumbnail to Children'
|
||||||
|
# Action icon
|
||||||
|
icon = "https://cdn3.iconfinder.com/data/icons/transfers/100/239322-download_transfer-128.png"
|
||||||
|
|
||||||
|
|
||||||
|
def discover(self, session, entities, event):
|
||||||
|
''' Validation '''
|
||||||
|
|
||||||
|
if (len(entities) <= 0 or entities[0].entity_type in ['Project']):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def launch(self, session, entities, event):
|
||||||
|
'''Callback method for action.'''
|
||||||
|
|
||||||
|
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': 'Push thumbnails to Childrens'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
for entity in entities:
|
||||||
|
thumbid = entity['thumbnail_id']
|
||||||
|
if thumbid:
|
||||||
|
for child in entity['children']:
|
||||||
|
child['thumbnail_id'] = thumbid
|
||||||
|
|
||||||
|
# inform the user that the job is done
|
||||||
|
job['status'] = 'done'
|
||||||
|
session.commit()
|
||||||
|
except:
|
||||||
|
# fail the job if something goes wrong
|
||||||
|
job['status'] = 'failed'
|
||||||
|
raise
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Created job for updating thumbnails!'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def register(session, **kw):
|
||||||
|
'''Register action. Called when used as an event plugin.'''
|
||||||
|
if not isinstance(session, ftrack_api.session.Session):
|
||||||
|
return
|
||||||
|
|
||||||
|
action_handler = ThumbToChildren(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:]))
|
||||||
122
pype/ftrack/actions/action_thumbToParent.py
Normal file
122
pype/ftrack/actions/action_thumbToParent.py
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
# :coding: utf-8
|
||||||
|
# :copyright: Copyright (c) 2015 Milan Kolar
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import getpass
|
||||||
|
import json
|
||||||
|
import ftrack_api
|
||||||
|
from ftrack_action_handler import BaseAction
|
||||||
|
|
||||||
|
class ThumbToParent(BaseAction):
|
||||||
|
'''Custom action.'''
|
||||||
|
|
||||||
|
# Action identifier
|
||||||
|
identifier = 'thumb.to.parent'
|
||||||
|
# Action label
|
||||||
|
label = 'Thumbnail to Parent'
|
||||||
|
# Action icon
|
||||||
|
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.'''
|
||||||
|
|
||||||
|
if len(entities) <= 0 or entities[0].entity_type in ['Project']:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def launch(self, session, entities, event):
|
||||||
|
'''Callback method for action.'''
|
||||||
|
|
||||||
|
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': 'Push thumbnails to parents'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
for entity in entities:
|
||||||
|
if entity.entity_type.lower() == 'assetversion':
|
||||||
|
try:
|
||||||
|
parent = entity['task']
|
||||||
|
except:
|
||||||
|
par_ent = entity['link'][-2]
|
||||||
|
parent = session.get(par_ent['type'], par_ent['id'])
|
||||||
|
|
||||||
|
elif entity.entity_type.lower() == 'task':
|
||||||
|
parent = entity['parent']
|
||||||
|
|
||||||
|
thumbid = entity['thumbnail_id']
|
||||||
|
|
||||||
|
if parent and thumbid:
|
||||||
|
parent['thumbnail_id'] = thumbid
|
||||||
|
|
||||||
|
# inform the user that the job is done
|
||||||
|
job['status'] = 'done'
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
except:
|
||||||
|
# fail the job if something goes wrong
|
||||||
|
job['status'] = 'failed'
|
||||||
|
raise
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Created job for updating thumbnails!'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register(session, **kw):
|
||||||
|
'''Register action. Called when used as an event plugin.'''
|
||||||
|
if not isinstance(session, ftrack_api.session.Session):
|
||||||
|
return
|
||||||
|
|
||||||
|
action_handler = ThumbToParent(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:]))
|
||||||
379
pype/ftrack/actions/djvview.py
Normal file
379
pype/ftrack/actions/djvview.py
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from operator import itemgetter
|
||||||
|
import ftrack_api
|
||||||
|
|
||||||
|
|
||||||
|
class DJVViewAction(object):
|
||||||
|
"""Launch DJVView action."""
|
||||||
|
identifier = "djvview-launch-action"
|
||||||
|
# label = "DJV View"
|
||||||
|
# icon = "http://a.fsdn.com/allura/p/djv/icon"
|
||||||
|
|
||||||
|
def __init__(self, session):
|
||||||
|
'''Expects a ftrack_api.Session instance'''
|
||||||
|
|
||||||
|
self.logger = logging.getLogger(
|
||||||
|
'{0}.{1}'.format(__name__, self.__class__.__name__)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.identifier is None:
|
||||||
|
raise ValueError(
|
||||||
|
'Action missing identifier.'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.session = session
|
||||||
|
|
||||||
|
def is_valid_selection(self, event):
|
||||||
|
selection = event["data"].get("selection", [])
|
||||||
|
|
||||||
|
if not selection:
|
||||||
|
return
|
||||||
|
|
||||||
|
entityType = selection[0]["entityType"]
|
||||||
|
|
||||||
|
if entityType not in ["assetversion", "task"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def discover(self, event):
|
||||||
|
"""Return available actions based on *event*. """
|
||||||
|
|
||||||
|
if not self.is_valid_selection(event):
|
||||||
|
return
|
||||||
|
|
||||||
|
items = []
|
||||||
|
applications = self.get_applications()
|
||||||
|
applications = sorted(
|
||||||
|
applications, key=lambda application: application["label"]
|
||||||
|
)
|
||||||
|
|
||||||
|
for application in applications:
|
||||||
|
self.djv_path = application.get("path", None)
|
||||||
|
applicationIdentifier = application["identifier"]
|
||||||
|
label = application["label"]
|
||||||
|
items.append({
|
||||||
|
"actionIdentifier": self.identifier,
|
||||||
|
"label": label,
|
||||||
|
"variant": application.get("variant", None),
|
||||||
|
"description": application.get("description", None),
|
||||||
|
"icon": application.get("icon", "default"),
|
||||||
|
"applicationIdentifier": applicationIdentifier
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": items
|
||||||
|
}
|
||||||
|
|
||||||
|
def register(self):
|
||||||
|
'''Registers the action, subscribing the discover and launch topics.'''
|
||||||
|
self.session.event_hub.subscribe(
|
||||||
|
'topic=ftrack.action.discover and source.user.username={0}'.format(
|
||||||
|
self.session.api_user
|
||||||
|
), self.discover
|
||||||
|
)
|
||||||
|
|
||||||
|
self.session.event_hub.subscribe(
|
||||||
|
'topic=ftrack.action.launch and data.actionIdentifier={0} and source.user.username={1}'.format(
|
||||||
|
self.identifier,
|
||||||
|
self.session.api_user
|
||||||
|
),
|
||||||
|
self.launch
|
||||||
|
)
|
||||||
|
print("----- action - <" + self.__class__.__name__ + "> - Has been registered -----")
|
||||||
|
|
||||||
|
def get_applications(self):
|
||||||
|
applications = []
|
||||||
|
|
||||||
|
label = "DJVView {version}"
|
||||||
|
versionExpression = re.compile(r"(?P<version>\d+.\d+.\d+)")
|
||||||
|
applicationIdentifier = "djvview"
|
||||||
|
description = "DJV View Launcher"
|
||||||
|
icon = "http://a.fsdn.com/allura/p/djv/icon"
|
||||||
|
expression = []
|
||||||
|
if sys.platform == "win32":
|
||||||
|
expression = ["C:\\", "Program Files", "djv-\d.+",
|
||||||
|
"bin", "djv_view.exe"]
|
||||||
|
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
expression = ["Application", "DJV.app", "Contents", "MacOS", "DJV"]
|
||||||
|
# Linuxs
|
||||||
|
else:
|
||||||
|
expression = ["usr", "local", "djv", "djv_view"]
|
||||||
|
|
||||||
|
pieces = expression[:]
|
||||||
|
start = pieces.pop(0)
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
# On Windows C: means current directory so convert roots that look
|
||||||
|
# like drive letters to the C:\ format.
|
||||||
|
if start and start[-1] == ':':
|
||||||
|
start += '\\'
|
||||||
|
|
||||||
|
if not os.path.exists(start):
|
||||||
|
raise ValueError(
|
||||||
|
'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):
|
||||||
|
level = location.rstrip(os.path.sep).count(os.path.sep)
|
||||||
|
expression = expressions[level]
|
||||||
|
|
||||||
|
if level < (expressionsCount - 1):
|
||||||
|
# If not yet at final piece then just prune directories.
|
||||||
|
folders[:] = [folder for folder in folders
|
||||||
|
if expression.match(folder)]
|
||||||
|
else:
|
||||||
|
# Match executable. Note that on OSX executable might equate to
|
||||||
|
# a folder (.app).
|
||||||
|
for entry in folders + files:
|
||||||
|
match = expression.match(entry)
|
||||||
|
if match:
|
||||||
|
# Extract version from full matching path.
|
||||||
|
path = os.path.join(start, location, entry)
|
||||||
|
versionMatch = versionExpression.search(path)
|
||||||
|
if versionMatch:
|
||||||
|
version = versionMatch.group('version')
|
||||||
|
|
||||||
|
applications.append({
|
||||||
|
'identifier': applicationIdentifier.format(
|
||||||
|
version=version
|
||||||
|
),
|
||||||
|
'path': path,
|
||||||
|
'version': version,
|
||||||
|
'label': label.format(version=version),
|
||||||
|
'icon': icon,
|
||||||
|
# 'variant': variant.format(version=version),
|
||||||
|
'description': description
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.logger.debug(
|
||||||
|
'Discovered application executable, but it '
|
||||||
|
'does not appear to o contain required version '
|
||||||
|
'information: {0}'.format(path)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Don't descend any further as out of patterns to match.
|
||||||
|
del folders[:]
|
||||||
|
|
||||||
|
return applications
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def get_entity_type(self, entity):
|
||||||
|
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):
|
||||||
|
"""Callback method for DJVView action."""
|
||||||
|
session = self.session
|
||||||
|
entities = self.translate_event(session, event)
|
||||||
|
|
||||||
|
# Launching application
|
||||||
|
if "values" in event["data"]:
|
||||||
|
|
||||||
|
filename = event['data']['values']['path']
|
||||||
|
|
||||||
|
# TODO These should be obtained in another way
|
||||||
|
start = 375
|
||||||
|
end = 379
|
||||||
|
fps = 24
|
||||||
|
# TODO issequence is probably already built-in validation in ftrack
|
||||||
|
isseq = re.findall('%[0-9]*d', filename)
|
||||||
|
if len(isseq) > 0:
|
||||||
|
padding = re.findall('%[0-9]*d', filename).pop()
|
||||||
|
range = (padding % start) + '-' + (padding % end)
|
||||||
|
filename = re.sub('%[0-9]*d', range, filename)
|
||||||
|
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
# DJV Options End ################################################
|
||||||
|
|
||||||
|
# PATH TO COMPONENT
|
||||||
|
cmd.append(os.path.normpath(filename))
|
||||||
|
|
||||||
|
# Run DJV with these commands
|
||||||
|
subprocess.Popen(' '.join(cmd))
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'DJV View started.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if 'items' not in event["data"]:
|
||||||
|
event["data"]['items'] = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for entity in entities:
|
||||||
|
versions = []
|
||||||
|
allowed_types = ["img", "mov", "exr"]
|
||||||
|
|
||||||
|
if entity.entity_type.lower() == "assetversion":
|
||||||
|
if entity['components'][0]['file_type'] in allowed_types:
|
||||||
|
versions.append(entity)
|
||||||
|
|
||||||
|
if entity.entity_type.lower() == "task":
|
||||||
|
# AssetVersions are obtainable only from shot!
|
||||||
|
shotentity = entity['parent']
|
||||||
|
|
||||||
|
for asset in shotentity['assets']:
|
||||||
|
for version in asset['versions']:
|
||||||
|
# Get only AssetVersion of selected task
|
||||||
|
if version['task']['id'] != entity['id']:
|
||||||
|
continue
|
||||||
|
# Get only components with allowed type
|
||||||
|
if version['components'][0]['file_type'] in allowed_types:
|
||||||
|
versions.append(version)
|
||||||
|
|
||||||
|
# Raise error if no components were found
|
||||||
|
if len(versions) < 1:
|
||||||
|
raise ValueError('There are no Asset Versions to open.')
|
||||||
|
|
||||||
|
for version in versions:
|
||||||
|
for component in version['components']:
|
||||||
|
label = "v{0} - {1} - {2}"
|
||||||
|
|
||||||
|
label = label.format(
|
||||||
|
str(version['version']).zfill(3),
|
||||||
|
version['asset']['type']['name'],
|
||||||
|
component['name']
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# TODO This is proper way to get filepath!!!
|
||||||
|
# THIS WON'T WORK RIGHT NOW
|
||||||
|
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())
|
||||||
|
# file_path = file_path % frame
|
||||||
|
except:
|
||||||
|
# This works but is NOT proper way
|
||||||
|
file_path = component['component_locations'][0]['resource_identifier']
|
||||||
|
|
||||||
|
event["data"]["items"].append(
|
||||||
|
{"label": label, "value": file_path}
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"label": "Items to view",
|
||||||
|
"type": "enumerator",
|
||||||
|
"name": "path",
|
||||||
|
"data": sorted(
|
||||||
|
event["data"]['items'],
|
||||||
|
key=itemgetter("label"),
|
||||||
|
reverse=True
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def register(session, **kw):
|
||||||
|
"""Register hooks."""
|
||||||
|
if not isinstance(session, ftrack_api.session.Session):
|
||||||
|
return
|
||||||
|
|
||||||
|
action = DJVViewAction(session)
|
||||||
|
action.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:]))
|
||||||
428
pype/ftrack/actions/ft_utils.py
Normal file
428
pype/ftrack/actions/ft_utils.py
Normal file
|
|
@ -0,0 +1,428 @@
|
||||||
|
import os
|
||||||
|
import operator
|
||||||
|
import ftrack_api
|
||||||
|
import collections
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
@ -19,7 +19,6 @@ t = Templates(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AppAction(object):
|
class AppAction(object):
|
||||||
'''Custom Action base class
|
'''Custom Action base class
|
||||||
|
|
||||||
|
|
@ -53,14 +52,13 @@ class AppAction(object):
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def session(self):
|
def session(self):
|
||||||
'''Return current session.'''
|
'''Return current session.'''
|
||||||
return self._session
|
return self._session
|
||||||
|
|
||||||
def register(self):
|
def register(self):
|
||||||
'''Registers the action, subscribing the the discover and launch topics.'''
|
'''Registers the action, subscribing the discover and launch topics.'''
|
||||||
self.session.event_hub.subscribe(
|
self.session.event_hub.subscribe(
|
||||||
'topic=ftrack.action.discover and source.user.username={0}'.format(
|
'topic=ftrack.action.discover and source.user.username={0}'.format(
|
||||||
self.session.api_user
|
self.session.api_user
|
||||||
|
|
@ -85,6 +83,7 @@ class AppAction(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
if accepts:
|
if accepts:
|
||||||
|
self.logger.info('Selection is valid')
|
||||||
return {
|
return {
|
||||||
'items': [{
|
'items': [{
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
|
|
@ -94,17 +93,18 @@ class AppAction(object):
|
||||||
'icon': self.icon,
|
'icon': self.icon,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
self.logger.info('Selection is _not_ valid')
|
||||||
|
|
||||||
def discover(self, session, entities, event):
|
def discover(self, session, entities, event):
|
||||||
'''Return true if we can handle the selected entities.
|
'''Return true if we can handle the selected entities.
|
||||||
|
|
||||||
*session* is a `ftrack_api.Session` instance
|
*session* is a `ftrack_api.Session` instance
|
||||||
|
|
||||||
|
*entities* is a list of tuples each containing the entity type and
|
||||||
*entities* is a list of tuples each containing the entity type and the entity id.
|
the entity id. If the entity is a hierarchical you will always get
|
||||||
If the entity is a hierarchical you will always get the entity
|
the entity type TypedContext, once retrieved through a get operation
|
||||||
type TypedContext, once retrieved through a get operation you
|
you will have the "real" entity type ie. example Shot, Sequence
|
||||||
will have the "real" entity type ie. example Shot, Sequence
|
|
||||||
or Asset Build.
|
or Asset Build.
|
||||||
|
|
||||||
*event* the unmodified original event
|
*event* the unmodified original event
|
||||||
|
|
@ -221,6 +221,7 @@ class AppAction(object):
|
||||||
*event* the unmodified original event
|
*event* the unmodified original event
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# TODO Delete this line
|
# TODO Delete this line
|
||||||
print("Action - {0} ({1}) - just started".format(self.label, self.identifier))
|
print("Action - {0} ({1}) - just started".format(self.label, self.identifier))
|
||||||
|
|
||||||
|
|
@ -228,8 +229,8 @@ class AppAction(object):
|
||||||
entity = session.get(entity, id)
|
entity = session.get(entity, id)
|
||||||
|
|
||||||
silo = "Film"
|
silo = "Film"
|
||||||
if entity.entity_type=="AssetBuild":
|
if entity.entity_type == "AssetBuild":
|
||||||
silo= "Asset"
|
silo = "Asset"
|
||||||
|
|
||||||
# set environments for Avalon
|
# set environments for Avalon
|
||||||
os.environ["AVALON_PROJECT"] = entity['project']['full_name']
|
os.environ["AVALON_PROJECT"] = entity['project']['full_name']
|
||||||
|
|
@ -239,24 +240,22 @@ class AppAction(object):
|
||||||
os.environ["AVALON_APP"] = self.identifier
|
os.environ["AVALON_APP"] = self.identifier
|
||||||
os.environ["AVALON_APP_NAME"] = self.identifier + "_" + self.variant
|
os.environ["AVALON_APP_NAME"] = self.identifier + "_" + self.variant
|
||||||
|
|
||||||
|
|
||||||
anatomy = t.anatomy
|
anatomy = t.anatomy
|
||||||
io.install()
|
io.install()
|
||||||
hierarchy = io.find_one({"type":'asset', "name":entity['parent']['name']})['data']['parents']
|
hierarchy = io.find_one({"type": 'asset', "name": entity['parent']['name']})['data']['parents']
|
||||||
io.uninstall()
|
io.uninstall()
|
||||||
if hierarchy:
|
if hierarchy:
|
||||||
# hierarchy = os.path.sep.join(hierarchy)
|
# hierarchy = os.path.sep.join(hierarchy)
|
||||||
hierarchy = os.path.join(*hierarchy)
|
hierarchy = os.path.join(*hierarchy)
|
||||||
|
|
||||||
data = { "project": {"name": entity['project']['full_name'],
|
data = {"project": {"name": entity['project']['full_name'],
|
||||||
"code": entity['project']['name']},
|
"code": entity['project']['name']},
|
||||||
"task": entity['name'],
|
"task": entity['name'],
|
||||||
"asset": entity['parent']['name'],
|
"asset": entity['parent']['name'],
|
||||||
"hierarchy": hierarchy}
|
"hierarchy": hierarchy}
|
||||||
|
|
||||||
anatomy = anatomy.format(data)
|
anatomy = anatomy.format(data)
|
||||||
|
|
||||||
|
|
||||||
os.environ["AVALON_WORKDIR"] = os.path.join(anatomy.work.root, anatomy.work.folder)
|
os.environ["AVALON_WORKDIR"] = os.path.join(anatomy.work.root, anatomy.work.folder)
|
||||||
|
|
||||||
# TODO Add paths to avalon setup from tomls
|
# TODO Add paths to avalon setup from tomls
|
||||||
|
|
@ -309,6 +308,13 @@ class AppAction(object):
|
||||||
'message': "We didn't found launcher for {0}".format(self.label)
|
'message': "We didn't found launcher for {0}".format(self.label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# RUN TIMER IN FTRACK
|
||||||
|
username = event['source']['user']['username']
|
||||||
|
user = session.query('User where username is "{}"'.format(username)).one()
|
||||||
|
task = session.query('Task where id is {}'.format(entity['id'])).one()
|
||||||
|
print('Starting timer for task: ' + task['name'])
|
||||||
|
user.start_timer(task, force=True)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': "Launching {0}".format(self.label)
|
'message': "Launching {0}".format(self.label)
|
||||||
|
|
@ -424,6 +430,7 @@ class BaseAction(object):
|
||||||
),
|
),
|
||||||
self._launch
|
self._launch
|
||||||
)
|
)
|
||||||
|
print("----- action - <" + self.__class__.__name__ + "> - Has been registered -----")
|
||||||
|
|
||||||
def _discover(self, event):
|
def _discover(self, event):
|
||||||
args = self._translate_event(
|
args = self._translate_event(
|
||||||
|
|
@ -435,6 +442,7 @@ class BaseAction(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
if accepts:
|
if accepts:
|
||||||
|
self.logger.info(u'Discovering action with selection: {0}'.format(args[1]['data'].get('selection', [])))
|
||||||
return {
|
return {
|
||||||
'items': [{
|
'items': [{
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
|
|
@ -472,7 +480,8 @@ class BaseAction(object):
|
||||||
for entity in _selection:
|
for entity in _selection:
|
||||||
_entities.append(
|
_entities.append(
|
||||||
(
|
(
|
||||||
self._get_entity_type(entity), entity.get('entityId')
|
session.get(self._get_entity_type(entity), entity.get('entityId'))
|
||||||
|
# self._get_entity_type(entity), entity.get('entityId')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
import ftrack_utils
|
|
||||||
|
|
||||||
import ftrack_api
|
|
||||||
|
|
||||||
|
|
||||||
session = ftrack_api.Session(
|
|
||||||
server_url="https://pype.ftrackapp.com",
|
|
||||||
api_key="4e01eda0-24b3-4451-8e01-70edc03286be",
|
|
||||||
api_user="jakub.trllo",
|
|
||||||
)
|
|
||||||
|
|
||||||
objTypes = set()
|
|
||||||
|
|
||||||
# TODO get all entity types ---- NOT TASK,MILESTONE,LIBRARY --> should be editable!!!
|
|
||||||
allObjTypes = session.query('ObjectType').all()
|
|
||||||
for object in range(len(allObjTypes)):
|
|
||||||
index = len(allObjTypes)-object-1
|
|
||||||
|
|
||||||
if (str(allObjTypes[index]['name']) in ['Task','Milestone','Library']):
|
|
||||||
allObjTypes.pop(index)
|
|
||||||
|
|
||||||
for k in allObjTypes:
|
|
||||||
print(k['name'])
|
|
||||||
|
|
||||||
# Name & Label for export Avalon-mongo ID to Ftrack
|
|
||||||
# allCustAttr = session.query('CustomAttributeConfiguration').all()
|
|
||||||
# curCustAttr = []
|
|
||||||
# for ca in allCustAttr:
|
|
||||||
# curCustAttr.append(ca['key'])
|
|
||||||
#
|
|
||||||
# custAttrName = 'avalon_mongo_id'
|
|
||||||
# custAttrLabel = 'Avalon/Mongo Id'
|
|
||||||
# custAttrType = session.query('CustomAttributeType where name is "text"').one()
|
|
||||||
# # TODO WHICH SECURITY ROLE IS RIGHT
|
|
||||||
# custAttrSecuRole = session.query('SecurityRole').all()
|
|
||||||
|
|
||||||
# for custAttrObjType in objTypes:
|
|
||||||
# # Create Custom attribute if not exists
|
|
||||||
# if custAttrName not in curCustAttr:
|
|
||||||
# session.create('CustomAttributeConfiguration', {
|
|
||||||
# 'entity_type': 'task',
|
|
||||||
# 'object_type_id': custAttrObjType['id'],
|
|
||||||
# 'type': custAttrType,
|
|
||||||
# 'label': custAttrLabel,
|
|
||||||
# 'key': custAttrName,
|
|
||||||
# 'default': '',
|
|
||||||
# 'write_security_roles': custAttrSecuRole,
|
|
||||||
# 'read_security_roles': custAttrSecuRole,
|
|
||||||
# 'config': json.dumps({'markdown': False}),
|
|
||||||
# })
|
|
||||||
# session.commit()
|
|
||||||
65
pype/ftrack/credentials.py
Normal file
65
pype/ftrack/credentials.py
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
import os
|
||||||
|
import toml
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if not os.path.isdir(folder):
|
||||||
|
os.makedirs(folder)
|
||||||
|
|
||||||
|
try:
|
||||||
|
file = open(fpath, 'r')
|
||||||
|
except:
|
||||||
|
filecreate = open(fpath, 'w')
|
||||||
|
filecreate.close()
|
||||||
|
file = open(fpath, 'r')
|
||||||
|
|
||||||
|
credentials = toml.load(file)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
def _save_credentials(username, apiKey):
|
||||||
|
file = open(fpath, 'w')
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'username':username,
|
||||||
|
'apiKey':apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials = toml.dumps(data)
|
||||||
|
file.write(credentials)
|
||||||
|
file.close()
|
||||||
|
|
||||||
|
def _clear_credentials():
|
||||||
|
file = open(fpath, 'w').close()
|
||||||
|
|
||||||
|
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:
|
||||||
|
_set_env(username, apiKey)
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = ftrack_api.Session()
|
||||||
|
session.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
56
pype/ftrack/ftrackRun.py
Normal file
56
pype/ftrack/ftrackRun.py
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from app.lib.utils import forward
|
||||||
|
from pype.ftrack import credentials, login_dialog as login_dialog
|
||||||
|
|
||||||
|
# Validation if alredy logged into Ftrack
|
||||||
|
def validate():
|
||||||
|
validation = False
|
||||||
|
cred = credentials._get_credentials()
|
||||||
|
if 'username' in cred and 'apiKey' in cred:
|
||||||
|
validation = credentials._check_credentials(
|
||||||
|
cred['username'],
|
||||||
|
cred['apiKey']
|
||||||
|
)
|
||||||
|
if validation is False:
|
||||||
|
login_dialog.run_login()
|
||||||
|
else:
|
||||||
|
login_dialog.run_login()
|
||||||
|
|
||||||
|
validation = credentials._check_credentials()
|
||||||
|
if not validation:
|
||||||
|
print("We are unable to connect to Ftrack")
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
# Entered arguments
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--actionserver", action="store_true",
|
||||||
|
help="launch action server for ftrack")
|
||||||
|
parser.add_argument("--eventserver", action="store_true",
|
||||||
|
help="launch action server for ftrack")
|
||||||
|
parser.add_argument("--logout", action="store_true",
|
||||||
|
help="launch action server for ftrack")
|
||||||
|
|
||||||
|
kwargs, args = parser.parse_known_args()
|
||||||
|
|
||||||
|
if kwargs.logout:
|
||||||
|
credentials._clear_credentials()
|
||||||
|
sys.exit()
|
||||||
|
else:
|
||||||
|
validate()
|
||||||
|
|
||||||
|
if kwargs.eventserver:
|
||||||
|
fname = os.path.join(os.environ["FTRACK_ACTION_SERVER"], "eventServer.py")
|
||||||
|
returncode = forward([
|
||||||
|
sys.executable, "-u", fname
|
||||||
|
])
|
||||||
|
|
||||||
|
else:
|
||||||
|
fname = os.path.join(os.environ["FTRACK_ACTION_SERVER"], "actionServer.py")
|
||||||
|
returncode = forward([
|
||||||
|
sys.executable, "-u", fname
|
||||||
|
])
|
||||||
|
|
||||||
|
sys.exit(returncode)
|
||||||
|
|
@ -1,62 +1,14 @@
|
||||||
# fttrack help functions
|
# fttrack help functions
|
||||||
|
|
||||||
|
import ftrack_api
|
||||||
# import ftrack
|
|
||||||
import os
|
import os
|
||||||
from pprint import *
|
from pprint import *
|
||||||
|
|
||||||
|
|
||||||
def deleteAssetsForTask(taskId):
|
def checkLogin():
|
||||||
#taskId = os.environ['FTRACK_TASKID']
|
# check Environments FTRACK_API_USER, FTRACK_API_KEY
|
||||||
task = ftrack.Task(taskId)
|
pass
|
||||||
|
|
||||||
taskAssets = task.getAssets()
|
|
||||||
print(taskAssets)
|
|
||||||
for a in taskAssets:
|
|
||||||
print(a.getName())
|
|
||||||
a.delete()
|
|
||||||
#shot = task.getParent()
|
|
||||||
#shotAssets = shot.getAssets()
|
|
||||||
|
|
||||||
|
|
||||||
def deleteAssetsFromShotByName(shotId, assNm=None):
|
|
||||||
if not assNm:
|
|
||||||
return
|
|
||||||
|
|
||||||
shot = ftrack.Task(shotId)
|
|
||||||
|
|
||||||
shotAssets = shot.getAssets()
|
|
||||||
for a in shotAssets:
|
|
||||||
nm = a.getName()
|
|
||||||
if nm == assNm:
|
|
||||||
a.delete()
|
|
||||||
|
|
||||||
# Created as action
|
|
||||||
def killRunningTasks(tm=None):
|
|
||||||
import datetime
|
|
||||||
import ftrack_api
|
|
||||||
|
|
||||||
session = ftrack_api.Session()
|
|
||||||
|
|
||||||
# Query all jobs created prior to yesterday which has not yet been completed.
|
|
||||||
yesterday = datetime.date.today() - datetime.timedelta(days=1)
|
|
||||||
if tm:
|
|
||||||
yesterday = tm
|
|
||||||
print(yesterday)
|
|
||||||
jobs = session.query(
|
|
||||||
'select id, status from Job '
|
|
||||||
'where status in ("queued", "running") and created_at > {0}'.format(yesterday)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update all the queried jobs, setting the status to failed.
|
|
||||||
for job in jobs:
|
|
||||||
print(job['created_at'])
|
|
||||||
print('Changing Job ({}) status: {} -> failed'.format(job['id'], job['status']))
|
|
||||||
job['status'] = 'failed'
|
|
||||||
|
|
||||||
session.commit()
|
|
||||||
|
|
||||||
print('Complete')
|
|
||||||
|
|
||||||
def checkRegex():
|
def checkRegex():
|
||||||
# _handle_result -> would be solution?
|
# _handle_result -> would be solution?
|
||||||
|
|
@ -83,3 +35,122 @@ def checkRegex():
|
||||||
'message': 'Entity name contains invalid character!'
|
'message': 'Entity name contains invalid character!'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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)
|
||||||
|
|
|
||||||
302
pype/ftrack/login_dialog.py
Normal file
302
pype/ftrack/login_dialog.py
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
from app import style
|
||||||
|
from . import credentials, login_tools
|
||||||
|
|
||||||
|
|
||||||
|
class Login_Dialog_ui(QtWidgets.QWidget):
|
||||||
|
|
||||||
|
SIZE_W = 300
|
||||||
|
SIZE_H = 230
|
||||||
|
|
||||||
|
loginSignal = QtCore.pyqtSignal(object, object, object)
|
||||||
|
_login_server_thread = None
|
||||||
|
inputs = []
|
||||||
|
buttons = []
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
|
self.loginSignal.connect(self.loginWithCredentials)
|
||||||
|
self._translate = QtCore.QCoreApplication.translate
|
||||||
|
|
||||||
|
self.font = QtGui.QFont()
|
||||||
|
self.font.setFamily("DejaVu Sans Condensed")
|
||||||
|
self.font.setPointSize(9)
|
||||||
|
self.font.setBold(True)
|
||||||
|
self.font.setWeight(50)
|
||||||
|
self.font.setKerning(True)
|
||||||
|
|
||||||
|
self.resize(self.SIZE_W, self.SIZE_H)
|
||||||
|
self.setMinimumSize(QtCore.QSize(self.SIZE_W, self.SIZE_H))
|
||||||
|
self.setStyleSheet(style.load_stylesheet())
|
||||||
|
|
||||||
|
self.setLayout(self._main())
|
||||||
|
self.setWindowTitle('FTrack Login')
|
||||||
|
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def _main(self):
|
||||||
|
self.main = QtWidgets.QVBoxLayout()
|
||||||
|
self.main.setObjectName("main")
|
||||||
|
|
||||||
|
self.form = QtWidgets.QFormLayout()
|
||||||
|
self.form.setContentsMargins(10, 15, 10, 5)
|
||||||
|
self.form.setObjectName("form")
|
||||||
|
|
||||||
|
self.ftsite_label = QtWidgets.QLabel("FTrack URL:")
|
||||||
|
self.ftsite_label.setFont(self.font)
|
||||||
|
self.ftsite_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||||
|
self.ftsite_label.setTextFormat(QtCore.Qt.RichText)
|
||||||
|
self.ftsite_label.setObjectName("user_label")
|
||||||
|
|
||||||
|
self.ftsite_input = QtWidgets.QLineEdit()
|
||||||
|
self.ftsite_input.setEnabled(True)
|
||||||
|
self.ftsite_input.setFrame(True)
|
||||||
|
self.ftsite_input.setEnabled(False)
|
||||||
|
self.ftsite_input.setReadOnly(True)
|
||||||
|
self.ftsite_input.setObjectName("ftsite_input")
|
||||||
|
|
||||||
|
self.user_label = QtWidgets.QLabel("Username:")
|
||||||
|
self.user_label.setFont(self.font)
|
||||||
|
self.user_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||||
|
self.user_label.setTextFormat(QtCore.Qt.RichText)
|
||||||
|
self.user_label.setObjectName("user_label")
|
||||||
|
|
||||||
|
self.user_input = QtWidgets.QLineEdit()
|
||||||
|
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.textChanged.connect(self._user_changed)
|
||||||
|
|
||||||
|
self.api_label = QtWidgets.QLabel("API Key:")
|
||||||
|
self.api_label.setFont(self.font)
|
||||||
|
self.api_label.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||||
|
self.api_label.setTextFormat(QtCore.Qt.RichText)
|
||||||
|
self.api_label.setObjectName("api_label")
|
||||||
|
|
||||||
|
self.api_input = QtWidgets.QLineEdit()
|
||||||
|
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.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.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.error_label)
|
||||||
|
|
||||||
|
self.btnGroup = QtWidgets.QHBoxLayout()
|
||||||
|
self.btnGroup.addStretch(1)
|
||||||
|
self.btnGroup.setObjectName("btnGroup")
|
||||||
|
|
||||||
|
self.btnEnter = QtWidgets.QPushButton("Login")
|
||||||
|
self.btnEnter.setToolTip('Set Username and API Key with entered values')
|
||||||
|
self.btnEnter.clicked.connect(self.enter_credentials)
|
||||||
|
|
||||||
|
self.btnClose = QtWidgets.QPushButton("Close")
|
||||||
|
self.btnClose.setToolTip('Close this window')
|
||||||
|
self.btnClose.clicked.connect(self._close_widget)
|
||||||
|
|
||||||
|
self.btnFtrack = QtWidgets.QPushButton("Ftrack")
|
||||||
|
self.btnFtrack.setToolTip('Open browser for Login to Ftrack')
|
||||||
|
self.btnFtrack.clicked.connect(self.open_ftrack)
|
||||||
|
|
||||||
|
self.btnGroup.addWidget(self.btnFtrack)
|
||||||
|
self.btnGroup.addWidget(self.btnEnter)
|
||||||
|
self.btnGroup.addWidget(self.btnClose)
|
||||||
|
|
||||||
|
self.main.addLayout(self.form)
|
||||||
|
self.main.addLayout(self.btnGroup)
|
||||||
|
|
||||||
|
self.inputs.append(self.api_input)
|
||||||
|
self.inputs.append(self.user_input)
|
||||||
|
self.inputs.append(self.ftsite_input)
|
||||||
|
|
||||||
|
self.enter_site()
|
||||||
|
return self.main
|
||||||
|
|
||||||
|
def enter_site(self):
|
||||||
|
try:
|
||||||
|
url = os.getenv('FTRACK_SERVER')
|
||||||
|
newurl = self.checkUrl(url)
|
||||||
|
|
||||||
|
if newurl is None:
|
||||||
|
self.btnEnter.setEnabled(False)
|
||||||
|
self.btnFtrack.setEnabled(False)
|
||||||
|
for input in self.inputs:
|
||||||
|
input.setEnabled(False)
|
||||||
|
newurl = url
|
||||||
|
|
||||||
|
self.ftsite_input.setText(newurl)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.setError("FTRACK_SERVER is not set in templates")
|
||||||
|
self.btnEnter.setEnabled(False)
|
||||||
|
self.btnFtrack.setEnabled(False)
|
||||||
|
for input in self.inputs:
|
||||||
|
input.setEnabled(False)
|
||||||
|
|
||||||
|
def setError(self, msg):
|
||||||
|
self.error_label.setText(msg)
|
||||||
|
self.error_label.show()
|
||||||
|
|
||||||
|
def _user_changed(self):
|
||||||
|
self.user_input.setStyleSheet("")
|
||||||
|
|
||||||
|
def _api_changed(self):
|
||||||
|
self.api_input.setStyleSheet("")
|
||||||
|
|
||||||
|
def _invalid_input(self,entity):
|
||||||
|
entity.setStyleSheet("border: 1px solid red;")
|
||||||
|
|
||||||
|
def enter_credentials(self):
|
||||||
|
user = self.user_input.text().strip()
|
||||||
|
api = self.api_input.text().strip()
|
||||||
|
msg = "You didn't enter "
|
||||||
|
missing = []
|
||||||
|
if user == "":
|
||||||
|
missing.append("Username")
|
||||||
|
self._invalid_input(self.user_input)
|
||||||
|
|
||||||
|
if api == "":
|
||||||
|
missing.append("API Key")
|
||||||
|
self._invalid_input(self.api_input)
|
||||||
|
|
||||||
|
if len(missing) > 0:
|
||||||
|
self.setError("{0} {1}".format(msg, " and ".join(missing)))
|
||||||
|
return
|
||||||
|
|
||||||
|
verification = credentials._check_credentials(user, api)
|
||||||
|
|
||||||
|
if verification:
|
||||||
|
credentials._save_credentials(username, apiKey)
|
||||||
|
credentials._set_env(username, apiKey)
|
||||||
|
self._close_widget()
|
||||||
|
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")
|
||||||
|
|
||||||
|
def open_ftrack(self):
|
||||||
|
url = self.ftsite_input.text()
|
||||||
|
self.loginWithCredentials(url,None,None)
|
||||||
|
|
||||||
|
def checkUrl(self, url):
|
||||||
|
url = url.strip('/ ')
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
self.setError("There is no URL set in Templates")
|
||||||
|
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:
|
||||||
|
self.setError(
|
||||||
|
'The server URL set in Templates could not be reached.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
|
||||||
|
):
|
||||||
|
self.setError(
|
||||||
|
'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:
|
||||||
|
self.setError(
|
||||||
|
'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:
|
||||||
|
self.setError(
|
||||||
|
'The server URL you provided could not be reached.'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.status_code != 200 or 'FTRACK_VERSION' not in result.headers
|
||||||
|
):
|
||||||
|
self.setError(
|
||||||
|
'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)
|
||||||
|
return
|
||||||
|
|
||||||
|
verification = credentials._check_credentials(username, apiKey)
|
||||||
|
|
||||||
|
if verification is True:
|
||||||
|
credentials._save_credentials(username, apiKey)
|
||||||
|
credentials._set_env(username, apiKey)
|
||||||
|
self._close_widget()
|
||||||
|
|
||||||
|
|
||||||
|
def _close_widget(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
class Login_Dialog(Login_Dialog_ui):
|
||||||
|
def __init__(self):
|
||||||
|
super(Login_Dialog, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
def getApp():
|
||||||
|
return QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
|
def run_login():
|
||||||
|
app = getApp()
|
||||||
|
ui = Login_Dialog()
|
||||||
|
ui.show()
|
||||||
|
app.exec_()
|
||||||
120
pype/ftrack/login_dialog_noui.py
Normal file
120
pype/ftrack/login_dialog_noui.py
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
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()
|
||||||
111
pype/ftrack/login_tools.py
Normal file
111
pype/ftrack/login_tools.py
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||||
|
from urllib import parse
|
||||||
|
import webbrowser
|
||||||
|
import functools
|
||||||
|
from PyQt5 import QtCore
|
||||||
|
|
||||||
|
# class LoginServerHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||||
|
class LoginServerHandler(BaseHTTPRequestHandler):
|
||||||
|
'''Login server handler.'''
|
||||||
|
|
||||||
|
def __init__(self, login_callback, *args, **kw):
|
||||||
|
'''Initialise handler.'''
|
||||||
|
self.login_callback = login_callback
|
||||||
|
BaseHTTPRequestHandler.__init__(self, *args, **kw)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
'''Override to handle requests ourselves.'''
|
||||||
|
parsed_path = parse.urlparse(self.path)
|
||||||
|
query = parsed_path.query
|
||||||
|
|
||||||
|
api_user = None
|
||||||
|
api_key = None
|
||||||
|
login_credentials = None
|
||||||
|
if 'api_user' and 'api_key' in query:
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
message = '<h1>Failed to sign in</h1>'
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(message.encode())
|
||||||
|
|
||||||
|
|
||||||
|
if login_credentials:
|
||||||
|
self.login_callback(
|
||||||
|
api_user,
|
||||||
|
api_key
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginServerThread(QtCore.QThread):
|
||||||
|
'''Login server thread.'''
|
||||||
|
|
||||||
|
# Login signal.
|
||||||
|
loginSignal = QtCore.pyqtSignal(object, object, object)
|
||||||
|
|
||||||
|
|
||||||
|
def start(self, url):
|
||||||
|
'''Start thread.'''
|
||||||
|
self.url = url
|
||||||
|
super(LoginServerThread, self).start()
|
||||||
|
|
||||||
|
def _handle_login(self, api_user, api_key):
|
||||||
|
'''Login to server with *api_user* and *api_key*.'''
|
||||||
|
self.loginSignal.emit(self.url, api_user, api_key)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
'''Listen for events.'''
|
||||||
|
# self._server = BaseHTTPServer.HTTPServer(
|
||||||
|
self._server = HTTPServer(
|
||||||
|
('localhost', 0),
|
||||||
|
functools.partial(
|
||||||
|
LoginServerHandler, self._handle_login
|
||||||
|
)
|
||||||
|
)
|
||||||
|
webbrowser.open_new_tab(
|
||||||
|
'{0}/user/api_credentials?redirect_url=http://localhost:{1}'.format(
|
||||||
|
self.url, self._server.server_port
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._server.handle_request()
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
# import ftrack_api as local session
|
|
||||||
import ftrack_api
|
|
||||||
#
|
|
||||||
session = ftrack_api.Session()
|
|
||||||
|
|
||||||
# ----------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def test_event(event):
|
|
||||||
'''just a testing event'''
|
|
||||||
# start of event procedure ----------------------------------
|
|
||||||
for entity in event['data'].get('entities', []):
|
|
||||||
print(100*"_")
|
|
||||||
print(entity['changes'])
|
|
||||||
|
|
||||||
# end of event procedure ----------------------------------
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
# import ftrack_api as local session
|
|
||||||
import ftrack_api
|
|
||||||
#
|
|
||||||
session = ftrack_api.Session()
|
|
||||||
|
|
||||||
# ----------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
def test_event(event):
|
|
||||||
'''just a testing event'''
|
|
||||||
# start of event procedure ----------------------------------
|
|
||||||
for entity in event['data'].get('entities', []):
|
|
||||||
print(100*"_")
|
|
||||||
print(entity['keys'])
|
|
||||||
|
|
||||||
# end of event procedure ----------------------------------
|
|
||||||
608
pype/vendor/appdirs.py
vendored
Normal file
608
pype/vendor/appdirs.py
vendored
Normal file
|
|
@ -0,0 +1,608 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2005-2010 ActiveState Software Inc.
|
||||||
|
# Copyright (c) 2013 Eddy Petrișor
|
||||||
|
|
||||||
|
"""Utilities for determining application-specific dirs.
|
||||||
|
|
||||||
|
See <http://github.com/ActiveState/appdirs> for details and usage.
|
||||||
|
"""
|
||||||
|
# Dev Notes:
|
||||||
|
# - MSDN on where to store app data files:
|
||||||
|
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
|
||||||
|
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
|
||||||
|
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
||||||
|
|
||||||
|
__version_info__ = (1, 4, 3)
|
||||||
|
__version__ = '.'.join(map(str, __version_info__))
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] == 3
|
||||||
|
|
||||||
|
if PY3:
|
||||||
|
unicode = str
|
||||||
|
|
||||||
|
if sys.platform.startswith('java'):
|
||||||
|
import platform
|
||||||
|
os_name = platform.java_ver()[3][0]
|
||||||
|
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
|
||||||
|
system = 'win32'
|
||||||
|
elif os_name.startswith('Mac'): # "Mac OS X", etc.
|
||||||
|
system = 'darwin'
|
||||||
|
else: # "Linux", "SunOS", "FreeBSD", etc.
|
||||||
|
# Setting this to "linux2" is not ideal, but only Windows or Mac
|
||||||
|
# are actually checked for and the rest of the module expects
|
||||||
|
# *sys.platform* style strings.
|
||||||
|
system = 'linux2'
|
||||||
|
else:
|
||||||
|
system = sys.platform
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||||
|
r"""Return full path to the user-specific data dir for this application.
|
||||||
|
|
||||||
|
"appname" is the name of application.
|
||||||
|
If None, just the system directory is returned.
|
||||||
|
"appauthor" (only used on Windows) is the name of the
|
||||||
|
appauthor or distributing body for this application. Typically
|
||||||
|
it is the owning company name. This falls back to appname. You may
|
||||||
|
pass False to disable it.
|
||||||
|
"version" is an optional version path element to append to the
|
||||||
|
path. You might want to use this if you want multiple versions
|
||||||
|
of your app to be able to run independently. If used, this
|
||||||
|
would typically be "<major>.<minor>".
|
||||||
|
Only applied when appname is present.
|
||||||
|
"roaming" (boolean, default False) can be set True to use the Windows
|
||||||
|
roaming appdata directory. That means that for users on a Windows
|
||||||
|
network setup for roaming profiles, this user data will be
|
||||||
|
sync'd on login. See
|
||||||
|
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
||||||
|
for a discussion of issues.
|
||||||
|
|
||||||
|
Typical user data directories are:
|
||||||
|
Mac OS X: ~/Library/Application Support/<AppName>
|
||||||
|
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
|
||||||
|
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
|
||||||
|
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
|
||||||
|
Win 7 (not roaming): C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
|
||||||
|
Win 7 (roaming): C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
|
||||||
|
|
||||||
|
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
|
||||||
|
That means, by default "~/.local/share/<AppName>".
|
||||||
|
"""
|
||||||
|
if system == "win32":
|
||||||
|
if appauthor is None:
|
||||||
|
appauthor = appname
|
||||||
|
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
|
||||||
|
path = os.path.normpath(_get_win_folder(const))
|
||||||
|
if appname:
|
||||||
|
if appauthor is not False:
|
||||||
|
path = os.path.join(path, appauthor, appname)
|
||||||
|
else:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
elif system == 'darwin':
|
||||||
|
path = os.path.expanduser('~/Library/Application Support/')
|
||||||
|
if appname:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
else:
|
||||||
|
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
|
||||||
|
if appname:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
if appname and version:
|
||||||
|
path = os.path.join(path, version)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
|
||||||
|
r"""Return full path to the user-shared data dir for this application.
|
||||||
|
|
||||||
|
"appname" is the name of application.
|
||||||
|
If None, just the system directory is returned.
|
||||||
|
"appauthor" (only used on Windows) is the name of the
|
||||||
|
appauthor or distributing body for this application. Typically
|
||||||
|
it is the owning company name. This falls back to appname. You may
|
||||||
|
pass False to disable it.
|
||||||
|
"version" is an optional version path element to append to the
|
||||||
|
path. You might want to use this if you want multiple versions
|
||||||
|
of your app to be able to run independently. If used, this
|
||||||
|
would typically be "<major>.<minor>".
|
||||||
|
Only applied when appname is present.
|
||||||
|
"multipath" is an optional parameter only applicable to *nix
|
||||||
|
which indicates that the entire list of data dirs should be
|
||||||
|
returned. By default, the first item from XDG_DATA_DIRS is
|
||||||
|
returned, or '/usr/local/share/<AppName>',
|
||||||
|
if XDG_DATA_DIRS is not set
|
||||||
|
|
||||||
|
Typical site data directories are:
|
||||||
|
Mac OS X: /Library/Application Support/<AppName>
|
||||||
|
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
|
||||||
|
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
|
||||||
|
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
|
||||||
|
Win 7: C:\ProgramData\<AppAuthor>\<AppName> # Hidden, but writeable on Win 7.
|
||||||
|
|
||||||
|
For Unix, this is using the $XDG_DATA_DIRS[0] default.
|
||||||
|
|
||||||
|
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
|
||||||
|
"""
|
||||||
|
if system == "win32":
|
||||||
|
if appauthor is None:
|
||||||
|
appauthor = appname
|
||||||
|
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
|
||||||
|
if appname:
|
||||||
|
if appauthor is not False:
|
||||||
|
path = os.path.join(path, appauthor, appname)
|
||||||
|
else:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
elif system == 'darwin':
|
||||||
|
path = os.path.expanduser('/Library/Application Support')
|
||||||
|
if appname:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
else:
|
||||||
|
# XDG default for $XDG_DATA_DIRS
|
||||||
|
# only first, if multipath is False
|
||||||
|
path = os.getenv('XDG_DATA_DIRS',
|
||||||
|
os.pathsep.join(['/usr/local/share', '/usr/share']))
|
||||||
|
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
|
||||||
|
if appname:
|
||||||
|
if version:
|
||||||
|
appname = os.path.join(appname, version)
|
||||||
|
pathlist = [os.sep.join([x, appname]) for x in pathlist]
|
||||||
|
|
||||||
|
if multipath:
|
||||||
|
path = os.pathsep.join(pathlist)
|
||||||
|
else:
|
||||||
|
path = pathlist[0]
|
||||||
|
return path
|
||||||
|
|
||||||
|
if appname and version:
|
||||||
|
path = os.path.join(path, version)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||||
|
r"""Return full path to the user-specific config dir for this application.
|
||||||
|
|
||||||
|
"appname" is the name of application.
|
||||||
|
If None, just the system directory is returned.
|
||||||
|
"appauthor" (only used on Windows) is the name of the
|
||||||
|
appauthor or distributing body for this application. Typically
|
||||||
|
it is the owning company name. This falls back to appname. You may
|
||||||
|
pass False to disable it.
|
||||||
|
"version" is an optional version path element to append to the
|
||||||
|
path. You might want to use this if you want multiple versions
|
||||||
|
of your app to be able to run independently. If used, this
|
||||||
|
would typically be "<major>.<minor>".
|
||||||
|
Only applied when appname is present.
|
||||||
|
"roaming" (boolean, default False) can be set True to use the Windows
|
||||||
|
roaming appdata directory. That means that for users on a Windows
|
||||||
|
network setup for roaming profiles, this user data will be
|
||||||
|
sync'd on login. See
|
||||||
|
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
||||||
|
for a discussion of issues.
|
||||||
|
|
||||||
|
Typical user config directories are:
|
||||||
|
Mac OS X: same as user_data_dir
|
||||||
|
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
|
||||||
|
Win *: same as user_data_dir
|
||||||
|
|
||||||
|
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
|
||||||
|
That means, by default "~/.config/<AppName>".
|
||||||
|
"""
|
||||||
|
if system in ["win32", "darwin"]:
|
||||||
|
path = user_data_dir(appname, appauthor, None, roaming)
|
||||||
|
else:
|
||||||
|
path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
|
||||||
|
if appname:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
if appname and version:
|
||||||
|
path = os.path.join(path, version)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
|
||||||
|
r"""Return full path to the user-shared data dir for this application.
|
||||||
|
|
||||||
|
"appname" is the name of application.
|
||||||
|
If None, just the system directory is returned.
|
||||||
|
"appauthor" (only used on Windows) is the name of the
|
||||||
|
appauthor or distributing body for this application. Typically
|
||||||
|
it is the owning company name. This falls back to appname. You may
|
||||||
|
pass False to disable it.
|
||||||
|
"version" is an optional version path element to append to the
|
||||||
|
path. You might want to use this if you want multiple versions
|
||||||
|
of your app to be able to run independently. If used, this
|
||||||
|
would typically be "<major>.<minor>".
|
||||||
|
Only applied when appname is present.
|
||||||
|
"multipath" is an optional parameter only applicable to *nix
|
||||||
|
which indicates that the entire list of config dirs should be
|
||||||
|
returned. By default, the first item from XDG_CONFIG_DIRS is
|
||||||
|
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
|
||||||
|
|
||||||
|
Typical site config directories are:
|
||||||
|
Mac OS X: same as site_data_dir
|
||||||
|
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
|
||||||
|
$XDG_CONFIG_DIRS
|
||||||
|
Win *: same as site_data_dir
|
||||||
|
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
|
||||||
|
|
||||||
|
For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
|
||||||
|
|
||||||
|
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
|
||||||
|
"""
|
||||||
|
if system in ["win32", "darwin"]:
|
||||||
|
path = site_data_dir(appname, appauthor)
|
||||||
|
if appname and version:
|
||||||
|
path = os.path.join(path, version)
|
||||||
|
else:
|
||||||
|
# XDG default for $XDG_CONFIG_DIRS
|
||||||
|
# only first, if multipath is False
|
||||||
|
path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
|
||||||
|
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
|
||||||
|
if appname:
|
||||||
|
if version:
|
||||||
|
appname = os.path.join(appname, version)
|
||||||
|
pathlist = [os.sep.join([x, appname]) for x in pathlist]
|
||||||
|
|
||||||
|
if multipath:
|
||||||
|
path = os.pathsep.join(pathlist)
|
||||||
|
else:
|
||||||
|
path = pathlist[0]
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
|
||||||
|
r"""Return full path to the user-specific cache dir for this application.
|
||||||
|
|
||||||
|
"appname" is the name of application.
|
||||||
|
If None, just the system directory is returned.
|
||||||
|
"appauthor" (only used on Windows) is the name of the
|
||||||
|
appauthor or distributing body for this application. Typically
|
||||||
|
it is the owning company name. This falls back to appname. You may
|
||||||
|
pass False to disable it.
|
||||||
|
"version" is an optional version path element to append to the
|
||||||
|
path. You might want to use this if you want multiple versions
|
||||||
|
of your app to be able to run independently. If used, this
|
||||||
|
would typically be "<major>.<minor>".
|
||||||
|
Only applied when appname is present.
|
||||||
|
"opinion" (boolean) can be False to disable the appending of
|
||||||
|
"Cache" to the base app data dir for Windows. See
|
||||||
|
discussion below.
|
||||||
|
|
||||||
|
Typical user cache directories are:
|
||||||
|
Mac OS X: ~/Library/Caches/<AppName>
|
||||||
|
Unix: ~/.cache/<AppName> (XDG default)
|
||||||
|
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
|
||||||
|
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
|
||||||
|
|
||||||
|
On Windows the only suggestion in the MSDN docs is that local settings go in
|
||||||
|
the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
|
||||||
|
app data dir (the default returned by `user_data_dir` above). Apps typically
|
||||||
|
put cache data somewhere *under* the given dir here. Some examples:
|
||||||
|
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
|
||||||
|
...\Acme\SuperApp\Cache\1.0
|
||||||
|
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
|
||||||
|
This can be disabled with the `opinion=False` option.
|
||||||
|
"""
|
||||||
|
if system == "win32":
|
||||||
|
if appauthor is None:
|
||||||
|
appauthor = appname
|
||||||
|
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
|
||||||
|
if appname:
|
||||||
|
if appauthor is not False:
|
||||||
|
path = os.path.join(path, appauthor, appname)
|
||||||
|
else:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
if opinion:
|
||||||
|
path = os.path.join(path, "Cache")
|
||||||
|
elif system == 'darwin':
|
||||||
|
path = os.path.expanduser('~/Library/Caches')
|
||||||
|
if appname:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
else:
|
||||||
|
path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
|
||||||
|
if appname:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
if appname and version:
|
||||||
|
path = os.path.join(path, version)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def user_state_dir(appname=None, appauthor=None, version=None, roaming=False):
|
||||||
|
r"""Return full path to the user-specific state dir for this application.
|
||||||
|
|
||||||
|
"appname" is the name of application.
|
||||||
|
If None, just the system directory is returned.
|
||||||
|
"appauthor" (only used on Windows) is the name of the
|
||||||
|
appauthor or distributing body for this application. Typically
|
||||||
|
it is the owning company name. This falls back to appname. You may
|
||||||
|
pass False to disable it.
|
||||||
|
"version" is an optional version path element to append to the
|
||||||
|
path. You might want to use this if you want multiple versions
|
||||||
|
of your app to be able to run independently. If used, this
|
||||||
|
would typically be "<major>.<minor>".
|
||||||
|
Only applied when appname is present.
|
||||||
|
"roaming" (boolean, default False) can be set True to use the Windows
|
||||||
|
roaming appdata directory. That means that for users on a Windows
|
||||||
|
network setup for roaming profiles, this user data will be
|
||||||
|
sync'd on login. See
|
||||||
|
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
|
||||||
|
for a discussion of issues.
|
||||||
|
|
||||||
|
Typical user state directories are:
|
||||||
|
Mac OS X: same as user_data_dir
|
||||||
|
Unix: ~/.local/state/<AppName> # or in $XDG_STATE_HOME, if defined
|
||||||
|
Win *: same as user_data_dir
|
||||||
|
|
||||||
|
For Unix, we follow this Debian proposal <https://wiki.debian.org/XDGBaseDirectorySpecification#state>
|
||||||
|
to extend the XDG spec and support $XDG_STATE_HOME.
|
||||||
|
|
||||||
|
That means, by default "~/.local/state/<AppName>".
|
||||||
|
"""
|
||||||
|
if system in ["win32", "darwin"]:
|
||||||
|
path = user_data_dir(appname, appauthor, None, roaming)
|
||||||
|
else:
|
||||||
|
path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state"))
|
||||||
|
if appname:
|
||||||
|
path = os.path.join(path, appname)
|
||||||
|
if appname and version:
|
||||||
|
path = os.path.join(path, version)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
|
||||||
|
r"""Return full path to the user-specific log dir for this application.
|
||||||
|
|
||||||
|
"appname" is the name of application.
|
||||||
|
If None, just the system directory is returned.
|
||||||
|
"appauthor" (only used on Windows) is the name of the
|
||||||
|
appauthor or distributing body for this application. Typically
|
||||||
|
it is the owning company name. This falls back to appname. You may
|
||||||
|
pass False to disable it.
|
||||||
|
"version" is an optional version path element to append to the
|
||||||
|
path. You might want to use this if you want multiple versions
|
||||||
|
of your app to be able to run independently. If used, this
|
||||||
|
would typically be "<major>.<minor>".
|
||||||
|
Only applied when appname is present.
|
||||||
|
"opinion" (boolean) can be False to disable the appending of
|
||||||
|
"Logs" to the base app data dir for Windows, and "log" to the
|
||||||
|
base cache dir for Unix. See discussion below.
|
||||||
|
|
||||||
|
Typical user log directories are:
|
||||||
|
Mac OS X: ~/Library/Logs/<AppName>
|
||||||
|
Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
|
||||||
|
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
|
||||||
|
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
|
||||||
|
|
||||||
|
On Windows the only suggestion in the MSDN docs is that local settings
|
||||||
|
go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
|
||||||
|
examples of what some windows apps use for a logs dir.)
|
||||||
|
|
||||||
|
OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
|
||||||
|
value for Windows and appends "log" to the user cache dir for Unix.
|
||||||
|
This can be disabled with the `opinion=False` option.
|
||||||
|
"""
|
||||||
|
if system == "darwin":
|
||||||
|
path = os.path.join(
|
||||||
|
os.path.expanduser('~/Library/Logs'),
|
||||||
|
appname)
|
||||||
|
elif system == "win32":
|
||||||
|
path = user_data_dir(appname, appauthor, version)
|
||||||
|
version = False
|
||||||
|
if opinion:
|
||||||
|
path = os.path.join(path, "Logs")
|
||||||
|
else:
|
||||||
|
path = user_cache_dir(appname, appauthor, version)
|
||||||
|
version = False
|
||||||
|
if opinion:
|
||||||
|
path = os.path.join(path, "log")
|
||||||
|
if appname and version:
|
||||||
|
path = os.path.join(path, version)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class AppDirs(object):
|
||||||
|
"""Convenience wrapper for getting application dirs."""
|
||||||
|
def __init__(self, appname=None, appauthor=None, version=None,
|
||||||
|
roaming=False, multipath=False):
|
||||||
|
self.appname = appname
|
||||||
|
self.appauthor = appauthor
|
||||||
|
self.version = version
|
||||||
|
self.roaming = roaming
|
||||||
|
self.multipath = multipath
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_data_dir(self):
|
||||||
|
return user_data_dir(self.appname, self.appauthor,
|
||||||
|
version=self.version, roaming=self.roaming)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def site_data_dir(self):
|
||||||
|
return site_data_dir(self.appname, self.appauthor,
|
||||||
|
version=self.version, multipath=self.multipath)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_config_dir(self):
|
||||||
|
return user_config_dir(self.appname, self.appauthor,
|
||||||
|
version=self.version, roaming=self.roaming)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def site_config_dir(self):
|
||||||
|
return site_config_dir(self.appname, self.appauthor,
|
||||||
|
version=self.version, multipath=self.multipath)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_cache_dir(self):
|
||||||
|
return user_cache_dir(self.appname, self.appauthor,
|
||||||
|
version=self.version)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_state_dir(self):
|
||||||
|
return user_state_dir(self.appname, self.appauthor,
|
||||||
|
version=self.version)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def user_log_dir(self):
|
||||||
|
return user_log_dir(self.appname, self.appauthor,
|
||||||
|
version=self.version)
|
||||||
|
|
||||||
|
|
||||||
|
#---- internal support stuff
|
||||||
|
|
||||||
|
def _get_win_folder_from_registry(csidl_name):
|
||||||
|
"""This is a fallback technique at best. I'm not sure if using the
|
||||||
|
registry for this guarantees us the correct answer for all CSIDL_*
|
||||||
|
names.
|
||||||
|
"""
|
||||||
|
if PY3:
|
||||||
|
import winreg as _winreg
|
||||||
|
else:
|
||||||
|
import _winreg
|
||||||
|
|
||||||
|
shell_folder_name = {
|
||||||
|
"CSIDL_APPDATA": "AppData",
|
||||||
|
"CSIDL_COMMON_APPDATA": "Common AppData",
|
||||||
|
"CSIDL_LOCAL_APPDATA": "Local AppData",
|
||||||
|
}[csidl_name]
|
||||||
|
|
||||||
|
key = _winreg.OpenKey(
|
||||||
|
_winreg.HKEY_CURRENT_USER,
|
||||||
|
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
|
||||||
|
)
|
||||||
|
dir, type = _winreg.QueryValueEx(key, shell_folder_name)
|
||||||
|
return dir
|
||||||
|
|
||||||
|
|
||||||
|
def _get_win_folder_with_pywin32(csidl_name):
|
||||||
|
from win32com.shell import shellcon, shell
|
||||||
|
dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
|
||||||
|
# Try to make this a unicode path because SHGetFolderPath does
|
||||||
|
# not return unicode strings when there is unicode data in the
|
||||||
|
# path.
|
||||||
|
try:
|
||||||
|
dir = unicode(dir)
|
||||||
|
|
||||||
|
# Downgrade to short path name if have highbit chars. See
|
||||||
|
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||||
|
has_high_char = False
|
||||||
|
for c in dir:
|
||||||
|
if ord(c) > 255:
|
||||||
|
has_high_char = True
|
||||||
|
break
|
||||||
|
if has_high_char:
|
||||||
|
try:
|
||||||
|
import win32api
|
||||||
|
dir = win32api.GetShortPathName(dir)
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
except UnicodeError:
|
||||||
|
pass
|
||||||
|
return dir
|
||||||
|
|
||||||
|
|
||||||
|
def _get_win_folder_with_ctypes(csidl_name):
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
csidl_const = {
|
||||||
|
"CSIDL_APPDATA": 26,
|
||||||
|
"CSIDL_COMMON_APPDATA": 35,
|
||||||
|
"CSIDL_LOCAL_APPDATA": 28,
|
||||||
|
}[csidl_name]
|
||||||
|
|
||||||
|
buf = ctypes.create_unicode_buffer(1024)
|
||||||
|
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
|
||||||
|
|
||||||
|
# Downgrade to short path name if have highbit chars. See
|
||||||
|
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||||
|
has_high_char = False
|
||||||
|
for c in buf:
|
||||||
|
if ord(c) > 255:
|
||||||
|
has_high_char = True
|
||||||
|
break
|
||||||
|
if has_high_char:
|
||||||
|
buf2 = ctypes.create_unicode_buffer(1024)
|
||||||
|
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
|
||||||
|
buf = buf2
|
||||||
|
|
||||||
|
return buf.value
|
||||||
|
|
||||||
|
def _get_win_folder_with_jna(csidl_name):
|
||||||
|
import array
|
||||||
|
from com.sun import jna
|
||||||
|
from com.sun.jna.platform import win32
|
||||||
|
|
||||||
|
buf_size = win32.WinDef.MAX_PATH * 2
|
||||||
|
buf = array.zeros('c', buf_size)
|
||||||
|
shell = win32.Shell32.INSTANCE
|
||||||
|
shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
|
||||||
|
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
|
||||||
|
|
||||||
|
# Downgrade to short path name if have highbit chars. See
|
||||||
|
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
|
||||||
|
has_high_char = False
|
||||||
|
for c in dir:
|
||||||
|
if ord(c) > 255:
|
||||||
|
has_high_char = True
|
||||||
|
break
|
||||||
|
if has_high_char:
|
||||||
|
buf = array.zeros('c', buf_size)
|
||||||
|
kernel = win32.Kernel32.INSTANCE
|
||||||
|
if kernel.GetShortPathName(dir, buf, buf_size):
|
||||||
|
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
|
||||||
|
|
||||||
|
return dir
|
||||||
|
|
||||||
|
if system == "win32":
|
||||||
|
try:
|
||||||
|
import win32com.shell
|
||||||
|
_get_win_folder = _get_win_folder_with_pywin32
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from ctypes import windll
|
||||||
|
_get_win_folder = _get_win_folder_with_ctypes
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
import com.sun.jna
|
||||||
|
_get_win_folder = _get_win_folder_with_jna
|
||||||
|
except ImportError:
|
||||||
|
_get_win_folder = _get_win_folder_from_registry
|
||||||
|
|
||||||
|
|
||||||
|
#---- self test code
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
appname = "MyApp"
|
||||||
|
appauthor = "MyCompany"
|
||||||
|
|
||||||
|
props = ("user_data_dir",
|
||||||
|
"user_config_dir",
|
||||||
|
"user_cache_dir",
|
||||||
|
"user_state_dir",
|
||||||
|
"user_log_dir",
|
||||||
|
"site_data_dir",
|
||||||
|
"site_config_dir")
|
||||||
|
|
||||||
|
print("-- app dirs %s --" % __version__)
|
||||||
|
|
||||||
|
print("-- app dirs (with optional 'version')")
|
||||||
|
dirs = AppDirs(appname, appauthor, version="1.0")
|
||||||
|
for prop in props:
|
||||||
|
print("%s: %s" % (prop, getattr(dirs, prop)))
|
||||||
|
|
||||||
|
print("\n-- app dirs (without optional 'version')")
|
||||||
|
dirs = AppDirs(appname, appauthor)
|
||||||
|
for prop in props:
|
||||||
|
print("%s: %s" % (prop, getattr(dirs, prop)))
|
||||||
|
|
||||||
|
print("\n-- app dirs (without optional 'appauthor')")
|
||||||
|
dirs = AppDirs(appname)
|
||||||
|
for prop in props:
|
||||||
|
print("%s: %s" % (prop, getattr(dirs, prop)))
|
||||||
|
|
||||||
|
print("\n-- app dirs (with disabled 'appauthor')")
|
||||||
|
dirs = AppDirs(appname, appauthor=False)
|
||||||
|
for prop in props:
|
||||||
|
print("%s: %s" % (prop, getattr(dirs, prop)))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue