diff --git a/pype/api.py b/pype/api.py index 44a31f2626..ce13688d13 100644 --- a/pype/api.py +++ b/pype/api.py @@ -40,7 +40,8 @@ from .lib import ( get_version_from_path, get_last_version_from_path, modified_environ, - add_tool_to_environment + add_tool_to_environment, + get_latest_version ) # Special naming case for subprocess since its a built-in method. @@ -85,5 +86,6 @@ __all__ = [ "modified_environ", "add_tool_to_environment", - "subprocess" + "subprocess", + "get_latest_version" ] diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index a7204cba93..77d55eb1c1 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -158,6 +158,25 @@ class AExpectedFiles: """To be implemented by renderer class.""" pass + def sanitize_camera_name(self, camera): + """Sanitize camera name. + + Remove Maya illegal characters from camera name. + + Args: + camera (str): Maya camera name. + + Returns: + (str): sanitized camera name + + Example: + >>> sanizite_camera_name('test:camera_01') + test_camera_01 + + """ + sanitized = re.sub('[^0-9a-zA-Z_]+', '_', camera) + return sanitized + def get_renderer_prefix(self): """Return prefix for specific renderer. @@ -252,7 +271,7 @@ class AExpectedFiles: mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), + (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), # this is required to remove unfilled aov token, for example # in Redshift (R_REMOVE_AOV_TOKEN, ""), @@ -287,7 +306,8 @@ class AExpectedFiles: mappings = ( (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), + (R_SUBSTITUTE_CAMERA_TOKEN, + self.sanitize_camera_name(cam)), (R_SUBSTITUTE_AOV_TOKEN, aov[0]), (R_CLEAN_FRAME_TOKEN, ""), (R_CLEAN_EXT_TOKEN, ""), @@ -314,7 +334,8 @@ class AExpectedFiles: # camera name to AOV to allow per camera AOVs. aov_name = aov[0] if len(layer_data["cameras"]) > 1: - aov_name = "{}_{}".format(aov[0], cam) + aov_name = "{}_{}".format(aov[0], + self.sanitize_camera_name(cam)) aov_file_list[aov_name] = aov_files file_prefix = layer_data["filePrefix"] diff --git a/pype/hosts/nuke/lib.py b/pype/hosts/nuke/lib.py index 72a8836a03..8c0e37b15d 100644 --- a/pype/hosts/nuke/lib.py +++ b/pype/hosts/nuke/lib.py @@ -1445,7 +1445,7 @@ class ExporterReview: anlib.reset_selection() ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" in n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: diff --git a/pype/hosts/nukestudio/tags.json b/pype/hosts/nukestudio/tags.json new file mode 100644 index 0000000000..56fcfcbce9 --- /dev/null +++ b/pype/hosts/nukestudio/tags.json @@ -0,0 +1,262 @@ +{ + "Hierarchy": { + "editable": "1", + "note": "{folder}/{sequence}/{shot}", + "icon": { + "path": "hierarchy.png" + }, + "metadata": { + "folder": "FOLDER_NAME", + "shot": "{clip}", + "track": "{track}", + "sequence": "{sequence}", + "episode": "EPISODE_NAME", + "root": "{projectroot}" + } + }, + "Source Resolution": { + "editable": "1", + "note": "Use source resolution", + "icon": { + "path": "resolution.png" + }, + "metadata": { + "family": "resolution" + } + }, + "Retiming": { + "editable": "1", + "note": "Clip has retime or TimeWarp effects (or multiple effects stacked on the clip)", + "icon": { + "path": "retiming.png" + }, + "metadata": { + "family": "retiming", + "marginIn": 1, + "marginOut": 1 + } + }, + "Frame start": { + "editable": "1", + "note": "Starting frame for comps. \n\n> Use `value` and add either number or write `source` (if you want to preserve source frame numbering)", + "icon": { + "path": "icons:TagBackground.png" + }, + "metadata": { + "family": "frameStart", + "value": "1001" + } + }, + "[Lenses]": { + "Set lense here": { + "editable": "1", + "note": "Adjust parameters of your lense and then drop to clip. Remember! You can always overwrite on clip", + "icon": { + "path": "lense.png" + }, + "metadata": { + "focalLengthMm": 57 + + } + } + }, + "[Subsets]": { + "Audio": { + "editable": "1", + "note": "Export with Audio", + "icon": { + "path": "volume.png" + }, + "metadata": { + "family": "audio", + "subset": "main" + } + }, + "plateFg": { + "editable": "1", + "note": "Add to publish to \"forground\" subset. Change metadata subset name if different order number", + "icon": { + "path": "z_layer_fg.png" + }, + "metadata": { + "family": "plate", + "subset": "Fg01" + } + }, + "plateBg": { + "editable": "1", + "note": "Add to publish to \"background\" subset. Change metadata subset name if different order number", + "icon": { + "path": "z_layer_bg.png" + }, + "metadata": { + "family": "plate", + "subset": "Bg01" + } + }, + "plateRef": { + "editable": "1", + "note": "Add to publish to \"reference\" subset.", + "icon": { + "path": "icons:Reference.png" + }, + "metadata": { + "family": "plate", + "subset": "Ref" + } + }, + "plateMain": { + "editable": "1", + "note": "Add to publish to \"main\" subset.", + "icon": { + "path": "z_layer_main.png" + }, + "metadata": { + "family": "plate", + "subset": "main" + } + }, + "plateProxy": { + "editable": "1", + "note": "Add to publish to \"proxy\" subset.", + "icon": { + "path": "z_layer_main.png" + }, + "metadata": { + "family": "plate", + "subset": "proxy" + } + }, + "review": { + "editable": "1", + "note": "Upload to Ftrack as review component.", + "icon": { + "path": "review.png" + }, + "metadata": { + "family": "review", + "track": "review" + } + } + }, + "[Handles]": { + "start: add 20 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "20", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 10 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "10", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 5 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "5", + "args": "{'op':'add','where':'start'}" + } + }, + "start: add 0 frames": { + "editable": "1", + "note": "Adding frames to start of selected clip", + "icon": { + "path": "3_add_handles_start.png" + }, + "metadata": { + "family": "handles", + "value": "0", + "args": "{'op':'add','where':'start'}" + } + }, + "end: add 20 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "20", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 10 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "10", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 5 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "5", + "args": "{'op':'add','where':'end'}" + } + }, + "end: add 0 frames": { + "editable": "1", + "note": "Adding frames to end of selected clip", + "icon": { + "path": "1_add_handles_end.png" + }, + "metadata": { + "family": "handles", + "value": "0", + "args": "{'op':'add','where':'end'}" + } + } + }, + "NukeScript": { + "editable": "1", + "note": "Collecting track items to Nuke scripts.", + "icon": { + "path": "icons:TagNuke.png" + }, + "metadata": { + "family": "nukescript", + "subset": "main" + } + }, + "Comment": { + "editable": "1", + "note": "Comment on a shot.", + "icon": { + "path": "icons:TagComment.png" + }, + "metadata": { + "family": "comment", + "subset": "main" + } + } +} diff --git a/pype/hosts/nukestudio/tags.py b/pype/hosts/nukestudio/tags.py index c97f13d17c..c2b1d0d728 100644 --- a/pype/hosts/nukestudio/tags.py +++ b/pype/hosts/nukestudio/tags.py @@ -1,16 +1,22 @@ import re import os +import json import hiero -from pype.api import ( - config, - Logger -) +from pype.api import Logger from avalon import io log = Logger().get_logger(__name__, "nukestudio") +def tag_data(): + current_dir = os.path.dirname(__file__) + json_path = os.path.join(current_dir, "tags.json") + with open(json_path, "r") as json_stream: + data = json.load(json_stream) + return data + + def create_tag(key, value): """ Creating Tag object. @@ -58,13 +64,9 @@ def add_tags_from_presets(): return log.debug("Setting default tags on project: {}".format(project.name())) - - # get all presets - presets = config.get_presets() - # get nukestudio tag.json from presets - nks_pres = presets["nukestudio"] - nks_pres_tags = nks_pres.get("tags", None) + # get nukestudio tags.json + nks_pres_tags = tag_data() # Get project task types. tasks = io.find_one({"type": "project"})["config"]["tasks"] diff --git a/pype/lib.py b/pype/lib.py index 87808e53f5..f99cd73e09 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -520,14 +520,6 @@ def set_io_database(): io.install() -def get_all_avalon_projects(): - db = get_avalon_database() - projects = [] - for name in db.collection_names(): - projects.append(db[name].find_one({'type': 'project'})) - return projects - - def filter_pyblish_plugins(plugins): """ This servers as plugin filter / modifier for pyblish. It will load plugin @@ -1387,3 +1379,40 @@ def ffprobe_streams(path_to_file): popen_output = popen.communicate()[0] log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] + + +def get_latest_version(asset_name, subset_name): + """Retrieve latest version from `asset_name`, and `subset_name`. + + Args: + asset_name (str): Name of asset. + subset_name (str): Name of subset. + """ + # Get asset + asset_name = io.find_one( + {"type": "asset", "name": asset_name}, projection={"name": True} + ) + + subset = io.find_one( + {"type": "subset", "name": subset_name, "parent": asset_name["_id"]}, + projection={"_id": True, "name": True}, + ) + + # Check if subsets actually exists. + assert subset, "No subsets found." + + # Get version + version_projection = { + "name": True, + "parent": True, + } + + version = io.find_one( + {"type": "version", "parent": subset["_id"]}, + projection=version_projection, + sort=[("name", -1)], + ) + + assert version, "No version found, this is a bug" + + return version diff --git a/pype/modules/ftrack/actions/action_application_loader.py b/pype/modules/ftrack/actions/action_application_loader.py index ec7fc53fb6..ecc5a4fad3 100644 --- a/pype/modules/ftrack/actions/action_application_loader.py +++ b/pype/modules/ftrack/actions/action_application_loader.py @@ -3,8 +3,7 @@ import toml import time from pype.modules.ftrack.lib import AppAction from avalon import lib -from pype.api import Logger -from pype.lib import get_all_avalon_projects +from pype.api import Logger, config log = Logger().get_logger(__name__) @@ -49,17 +48,26 @@ def registerApp(app, session, plugins_presets): def register(session, plugins_presets={}): - # WARNING getting projects only helps to check connection to mongo - # - without will `discover` of ftrack apps actions take ages - result = get_all_avalon_projects() + app_usages = ( + config.get_presets() + .get("global", {}) + .get("applications") + ) or {} apps = [] - + missing_app_names = [] launchers_path = os.path.join(os.environ["PYPE_CONFIG"], "launchers") for file in os.listdir(launchers_path): filename, ext = os.path.splitext(file) if ext.lower() != ".toml": continue + + app_usage = app_usages.get(filename) + if not app_usage: + if app_usage is None: + missing_app_names.append(filename) + continue + loaded_data = toml.load(os.path.join(launchers_path, file)) app_data = { "name": filename, @@ -67,7 +75,17 @@ def register(session, plugins_presets={}): } apps.append(app_data) - apps = sorted(apps, key=lambda x: x['name']) + if missing_app_names: + log.debug( + "Apps not defined in applications usage. ({})".format( + ", ".join(( + "\"{}\"".format(app_name) + for app_name in missing_app_names + )) + ) + ) + + apps = sorted(apps, key=lambda app: app["name"]) app_counter = 0 for app in apps: try: @@ -76,7 +94,7 @@ def register(session, plugins_presets={}): time.sleep(0.1) app_counter += 1 except Exception as exc: - log.exception( + log.warning( "\"{}\" - not a proper App ({})".format(app['name'], str(exc)), exc_info=True ) diff --git a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py index 86503ff5bc..e81e587f0a 100644 --- a/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py +++ b/pype/modules/ftrack/actions/action_clean_hierarchical_attributes.py @@ -1,7 +1,7 @@ import collections import ftrack_api from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.avalon_sync import get_avalon_attr +from pype.modules.ftrack.lib.avalon_sync import get_pype_attr class CleanHierarchicalAttrsAction(BaseAction): @@ -48,7 +48,7 @@ class CleanHierarchicalAttrsAction(BaseAction): ) entity_ids_joined = ", ".join(all_entities_ids) - attrs, hier_attrs = get_avalon_attr(session) + attrs, hier_attrs = get_pype_attr(session) for attr in hier_attrs: configuration_key = attr["key"] diff --git a/pype/modules/ftrack/actions/action_create_cust_attrs.py b/pype/modules/ftrack/actions/action_create_cust_attrs.py index 9845cc8876..0c7e311377 100644 --- a/pype/modules/ftrack/actions/action_create_cust_attrs.py +++ b/pype/modules/ftrack/actions/action_create_cust_attrs.py @@ -1,99 +1,120 @@ +import os import collections +import toml import json import arrow import ftrack_api from pype.modules.ftrack.lib import BaseAction, statics_icon -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import ( + CUST_ATTR_ID_KEY, CUST_ATTR_GROUP, default_custom_attributes_definition +) from pype.api import config """ This action creates/updates custom attributes. -- first part take care about avalon_mongo_id attribute -- second part is based on json file in templates: - ~/PYPE-TEMPLATES/presets/ftrack/ftrack_custom_attributes.json - - you can add Custom attributes based on these conditions +## First part take care about special attributes + - `avalon_mongo_id` for storing Avalon MongoID + - `applications` based on applications usages + - `tools` based on tools usages + +## Second part is based on json file in ftrack module. +File location: `~/pype/pype/modules/ftrack/ftrack_custom_attributes.json` + +Data in json file is nested dictionary. Keys in first dictionary level +represents Ftrack entity type (task, show, assetversion, user, list, asset) +and dictionary value define attribute. + +There is special key for hierchical attributes `is_hierarchical`. + +Entity types `task` requires to define task object type (Folder, Shot, +Sequence, Task, Library, Milestone, Episode, Asset Build, etc.) at second +dictionary level, task's attributes are nested more. + +*** Not Changeable ********************************************************* + +group (string) + - name of group + - based on attribute `pype.modules.ftrack.lib.CUST_ATTR_GROUP` + - "pype" by default *** Required *************************************************************** label (string) - - label that will show in ftrack + - label that will show in ftrack key (string) - - must contain only chars [a-z0-9_] + - must contain only chars [a-z0-9_] type (string) - - type of custom attribute - - possibilities: text, boolean, date, enumerator, dynamic enumerator, number + - type of custom attribute + - possibilities: + text, boolean, date, enumerator, dynamic enumerator, number *** Required with conditions *********************************************** -entity_type (string) - - if 'is_hierarchical' is set to False - - type of entity - - possibilities: task, show, assetversion, user, list, asset - config (dictionary) - - for each entity type different requirements and possibilities: - - enumerator: multiSelect = True/False(default: False) - data = {key_1:value_1,key_2:value_2,..,key_n:value_n} - - 'data' is Required value with enumerator - - 'key' must contain only chars [a-z0-9_] + - for each attribute type different requirements and possibilities: + - enumerator: + multiSelect = True/False(default: False) + data = {key_1:value_1,key_2:value_2,..,key_n:value_n} + - 'data' is Required value with enumerator + - 'key' must contain only chars [a-z0-9_] - - number: isdecimal = True/False(default: False) + - number: + isdecimal = True/False(default: False) - - text: markdown = True/False(default: False) + - text: + markdown = True/False(default: False) -object_type (string) - - IF ENTITY_TYPE is set to 'task' - - default possibilities: Folder, Shot, Sequence, Task, Library, - Milestone, Episode, Asset Build,... - -*** Optional *************************************************************** +*** Presetable keys ********************************************************** write_security_roles/read_security_roles (array of strings) - - default: ["ALL"] - - strings should be role names (e.g.: ["API", "Administrator"]) - - if set to ["ALL"] - all roles will be availabled - - if first is 'except' - roles will be set to all except roles in array - - Warning: Be carefull with except - roles can be different by company - - example: - write_security_roles = ["except", "User"] - read_security_roles = ["ALL"] - - User is unable to write but can read - -group (string) - - default: None - - name of group + - default: ["ALL"] + - strings should be role names (e.g.: ["API", "Administrator"]) + - if set to ["ALL"] - all roles will be availabled + - if first is 'except' - roles will be set to all except roles in array + - Warning: Be carefull with except - roles can be different by company + - example: + write_security_roles = ["except", "User"] + read_security_roles = ["ALL"] # (User is can only read) default - - default: None - - sets default value for custom attribute: - - text -> string - - number -> integer - - enumerator -> array with string of key/s - - boolean -> bool true/false - - date -> string in format: 'YYYY.MM.DD' or 'YYYY.MM.DD HH:mm:ss' - - example: "2018.12.24" / "2018.1.1 6:0:0" - - dynamic enumerator -> DON'T HAVE DEFAULT VALUE!!! + - default: None + - sets default value for custom attribute: + - text -> string + - number -> integer + - enumerator -> array with string of key/s + - boolean -> bool true/false + - date -> string in format: 'YYYY.MM.DD' or 'YYYY.MM.DD HH:mm:ss' + - example: "2018.12.24" / "2018.1.1 6:0:0" + - dynamic enumerator -> DON'T HAVE DEFAULT VALUE!!! -is_hierarchical (bool) - - default: False - - will set hierachical attribute - - False by default - -EXAMPLE: -{ +Example: +``` +"show": { "avalon_auto_sync": { - "label": "Avalon auto-sync", - "key": "avalon_auto_sync", - "type": "boolean", - "entity_type": "show", - "group": "avalon", - "default": false, - "write_security_role": ["API","Administrator"], - "read_security_role": ["API","Administrator"] + "label": "Avalon auto-sync", + "type": "boolean", + "write_security_role": ["API", "Administrator"], + "read_security_role": ["API", "Administrator"] + } +}, +"is_hierarchical": { + "fps": { + "label": "FPS", + "type": "number", + "config": {"isdecimal": true} + } +}, +"task": { + "library": { + "my_attr_name": { + "label": "My Attr", + "type": "number" + } } } +``` """ @@ -115,11 +136,15 @@ class CustomAttributes(BaseAction): role_list = ['Pypeclub', 'Administrator'] icon = statics_icon("ftrack", "action_icons", "PypeAdmin.svg") - required_keys = ['key', 'label', 'type'] - type_posibilities = [ - 'text', 'boolean', 'date', 'enumerator', - 'dynamic enumerator', 'number' - ] + required_keys = ("key", "label", "type") + + presetable_keys = ("default", "write_security_role", "read_security_role") + hierarchical_key = "is_hierarchical" + + type_posibilities = ( + "text", "boolean", "date", "enumerator", + "dynamic enumerator", "number" + ) def discover(self, session, entities, event): ''' @@ -141,21 +166,24 @@ class CustomAttributes(BaseAction): }) }) session.commit() + try: self.prepare_global_data(session) self.avalon_mongo_id_attributes(session, event) - self.custom_attributes_from_file(session, event) + self.applications_attribute(event) + self.tools_attribute(event) + self.intent_attribute(event) + self.custom_attributes_from_file(event) job['status'] = 'done' session.commit() - except Exception as exc: + except Exception: session.rollback() - job['status'] = 'failed' + job["status"] = "failed" session.commit() self.log.error( - 'Creating custom attributes failed ({})'.format(exc), - exc_info=True + "Creating custom attributes failed ({})", exc_info=True ) return True @@ -182,20 +210,39 @@ class CustomAttributes(BaseAction): self.groups = {} + self.presets = config.get_presets() + self.attrs_presets = self.prepare_attribute_pressets() + + def prepare_attribute_pressets(self): + output = {} + + attr_presets = ( + self.presets.get("ftrack", {}).get("ftrack_custom_attributes") + ) or {} + for entity_type, preset in attr_presets.items(): + # Lower entity type + entity_type = entity_type.lower() + # Just store if entity type is not "task" + if entity_type != "task": + output[entity_type] = preset + continue + + # Prepare empty dictionary for entity type if not set yet + if entity_type not in output: + output[entity_type] = {} + + # Store presets per lowered object type + for obj_type, _preset in preset.items(): + output[entity_type][obj_type.lower()] = _preset + + return output + def avalon_mongo_id_attributes(self, session, event): + self.create_hierarchical_mongo_attr(session, event) + hierarchical_attr, object_type_attrs = ( self.mongo_id_custom_attributes(session) ) - - if hierarchical_attr is None: - self.create_hierarchical_mongo_attr(session) - hierarchical_attr, object_type_attrs = ( - self.mongo_id_custom_attributes(session) - ) - - if hierarchical_attr is None: - return - if object_type_attrs: self.convert_mongo_id_to_hierarchical( hierarchical_attr, object_type_attrs, session, event @@ -206,7 +253,7 @@ class CustomAttributes(BaseAction): "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" " where key = \"{}\"" - ).format(CustAttrIdKey) + ).format(CUST_ATTR_ID_KEY) mongo_id_avalon_attr = session.query(cust_attrs_query).all() heirarchical_attr = None @@ -220,32 +267,22 @@ class CustomAttributes(BaseAction): return heirarchical_attr, object_type_attrs - def create_hierarchical_mongo_attr(self, session): - # Attribute Name and Label - cust_attr_label = "Avalon/Mongo ID" - + def create_hierarchical_mongo_attr(self, session, event): # Set security roles for attribute - role_list = ("API", "Administrator", "Pypeclub") - roles = self.get_security_roles(role_list) - # Set Text type of Attribute - custom_attribute_type = self.types_per_name["text"] - # Set group to 'avalon' - group = self.get_group("avalon") - + default_role_list = ("API", "Administrator", "Pypeclub") data = { - "key": CustAttrIdKey, - "label": cust_attr_label, - "type": custom_attribute_type, + "key": CUST_ATTR_ID_KEY, + "label": "Avalon/Mongo ID", + "type": "text", "default": "", - "write_security_roles": roles, - "read_security_roles": roles, - "group": group, + "write_security_roles": default_role_list, + "read_security_roles": default_role_list, + "group": CUST_ATTR_GROUP, "is_hierarchical": True, - "entity_type": "show", - "config": json.dumps({"markdown": False}) + "config": {"markdown": False} } - self.process_attribute(data) + self.process_attr_data(data, event) def convert_mongo_id_to_hierarchical( self, hierarchical_attr, object_type_attrs, session, event @@ -335,91 +372,253 @@ class CustomAttributes(BaseAction): exc_info=True ) - def custom_attributes_from_file(self, session, event): - presets = config.get_presets()['ftrack']['ftrack_custom_attributes'] + def application_definitions(self): + app_usages = self.presets.get("global", {}).get("applications") or {} - for cust_attr_data in presets: - cust_attr_name = cust_attr_data.get( - 'label', - cust_attr_data.get('key') + app_definitions = [] + launchers_path = os.path.join(os.environ["PYPE_CONFIG"], "launchers") + + missing_app_names = [] + for file in os.listdir(launchers_path): + app_name, ext = os.path.splitext(file) + if ext.lower() != ".toml": + continue + + if not app_usages.get(app_name): + missing_app_names.append(app_name) + continue + + loaded_data = toml.load(os.path.join(launchers_path, file)) + + ftrack_label = loaded_data.get("ftrack_label") + if ftrack_label: + parts = app_name.split("_") + if len(parts) > 1: + ftrack_label = " ".join((ftrack_label, parts[-1])) + else: + ftrack_label = loaded_data.get("label", app_name) + + app_definitions.append({app_name: ftrack_label}) + + if missing_app_names: + self.log.warning( + "Apps not defined in applications usage. ({})".format( + ", ".join(( + "\"{}\"".format(app_name) + for app_name in missing_app_names + )) + ) ) - try: - data = {} - # Get key, label, type - data.update(self.get_required(cust_attr_data)) - # Get hierachical/ entity_type/ object_id - data.update(self.get_entity_type(cust_attr_data)) - # Get group, default, security roles - data.update(self.get_optional(cust_attr_data)) - # Process data - self.process_attribute(data) + return app_definitions - except CustAttrException as cae: - if cust_attr_name: - msg = 'Custom attribute error "{}" - {}'.format( - cust_attr_name, str(cae) - ) - else: - msg = 'Custom attribute error - {}'.format(str(cae)) - self.log.warning(msg, exc_info=True) - self.show_message(event, msg) + def applications_attribute(self, event): + applications_custom_attr_data = { + "label": "Applications", + "key": "applications", + "type": "enumerator", + "entity_type": "show", + "group": CUST_ATTR_GROUP, + "config": { + "multiselect": True, + "data": self.application_definitions() + } + } + self.process_attr_data(applications_custom_attr_data, event) - return True + def tools_attribute(self, event): + tool_usages = self.presets.get("global", {}).get("tools") or {} + tools_data = [] + for tool_name, usage in tool_usages.items(): + if usage: + tools_data.append({tool_name: tool_name}) + + tools_custom_attr_data = { + "label": "Tools", + "key": "tools_env", + "type": "enumerator", + "is_hierarchical": True, + "group": CUST_ATTR_GROUP, + "config": { + "multiselect": True, + "data": tools_data + } + } + self.process_attr_data(tools_custom_attr_data, event) + + def intent_attribute(self, event): + intent_key_values = ( + self.presets + .get("global", {}) + .get("intent", {}) + .get("items", {}) + ) or {} + + intent_values = [] + for key, label in intent_key_values.items(): + if not key or not label: + self.log.info(( + "Skipping intent row: {{\"{}\": \"{}\"}}" + " because of empty key or label." + ).format(key, label)) + continue + + intent_values.append({key: label}) + + if not intent_values: + return + + intent_custom_attr_data = { + "label": "Intent", + "key": "intent", + "type": "enumerator", + "entity_type": "assetversion", + "group": CUST_ATTR_GROUP, + "config": { + "multiselect": False, + "data": intent_values + } + } + self.process_attr_data(intent_custom_attr_data, event) + + def custom_attributes_from_file(self, event): + # Load json with custom attributes configurations + cust_attr_def = default_custom_attributes_definition() + attrs_data = [] + + # Prepare data of hierarchical attributes + hierarchical_attrs = cust_attr_def.pop(self.hierarchical_key, {}) + for key, cust_attr_data in hierarchical_attrs.items(): + cust_attr_data["key"] = key + cust_attr_data["is_hierarchical"] = True + attrs_data.append(cust_attr_data) + + # Prepare data of entity specific attributes + for entity_type, cust_attr_datas in cust_attr_def.items(): + if entity_type.lower() != "task": + for key, cust_attr_data in cust_attr_datas.items(): + cust_attr_data["key"] = key + cust_attr_data["entity_type"] = entity_type + attrs_data.append(cust_attr_data) + continue + + # Task should have nested level for object type + for object_type, _cust_attr_datas in cust_attr_datas.items(): + for key, cust_attr_data in _cust_attr_datas.items(): + cust_attr_data["key"] = key + cust_attr_data["entity_type"] = entity_type + cust_attr_data["object_type"] = object_type + attrs_data.append(cust_attr_data) + + # Process prepared data + for cust_attr_data in attrs_data: + # Add group + cust_attr_data["group"] = CUST_ATTR_GROUP + self.process_attr_data(cust_attr_data, event) + + def presets_for_attr_data(self, attr_data): + output = {} + + attr_key = attr_data["key"] + if attr_data.get("is_hierarchical"): + entity_key = self.hierarchical_key + else: + entity_key = attr_data["entity_type"] + + entity_presets = self.attrs_presets.get(entity_key) or {} + if entity_key.lower() == "task": + object_type = attr_data["object_type"] + entity_presets = entity_presets.get(object_type.lower()) or {} + + key_presets = entity_presets.get(attr_key) or {} + + for key, value in key_presets.items(): + if key in self.presetable_keys and value: + output[key] = value + return output + + def process_attr_data(self, cust_attr_data, event): + attr_presets = self.presets_for_attr_data(cust_attr_data) + cust_attr_data.update(attr_presets) + + try: + data = {} + # Get key, label, type + data.update(self.get_required(cust_attr_data)) + # Get hierachical/ entity_type/ object_id + data.update(self.get_entity_type(cust_attr_data)) + # Get group, default, security roles + data.update(self.get_optional(cust_attr_data)) + # Process data + self.process_attribute(data) + + except CustAttrException as cae: + cust_attr_name = cust_attr_data.get("label", cust_attr_data["key"]) + + if cust_attr_name: + msg = 'Custom attribute error "{}" - {}'.format( + cust_attr_name, str(cae) + ) + else: + msg = 'Custom attribute error - {}'.format(str(cae)) + self.log.warning(msg, exc_info=True) + self.show_message(event, msg) def process_attribute(self, data): - existing_atr = self.session.query('CustomAttributeConfiguration').all() + existing_attrs = self.session.query( + "CustomAttributeConfiguration" + ).all() matching = [] - for attr in existing_atr: + for attr in existing_attrs: if ( - attr['key'] != data['key'] or - attr['type']['name'] != data['type']['name'] + attr["key"] != data["key"] or + attr["type"]["name"] != data["type"]["name"] ): continue - if data.get('is_hierarchical', False) is True: - if attr['is_hierarchical'] is True: + if data.get("is_hierarchical") is True: + if attr["is_hierarchical"] is True: matching.append(attr) - elif 'object_type_id' in data: + elif "object_type_id" in data: if ( - attr['entity_type'] == data['entity_type'] and - attr['object_type_id'] == data['object_type_id'] + attr["entity_type"] == data["entity_type"] and + attr["object_type_id"] == data["object_type_id"] ): matching.append(attr) else: - if attr['entity_type'] == data['entity_type']: + if attr["entity_type"] == data["entity_type"]: matching.append(attr) if len(matching) == 0: - self.session.create('CustomAttributeConfiguration', data) + self.session.create("CustomAttributeConfiguration", data) self.session.commit() self.log.debug( - '{}: "{}" created'.format(self.label, data['label']) + "Custom attribute \"{}\" created".format(data["label"]) ) elif len(matching) == 1: attr_update = matching[0] for key in data: - if ( - key not in [ - 'is_hierarchical', 'entity_type', 'object_type_id' - ] + if key not in ( + "is_hierarchical", "entity_type", "object_type_id" ): attr_update[key] = data[key] - self.log.debug( - '{}: "{}" updated'.format(self.label, data['label']) - ) self.session.commit() + self.log.debug( + "Custom attribute \"{}\" updated".format(data["label"]) + ) else: - raise CustAttrException('Is duplicated') + raise CustAttrException(( + "Custom attribute is duplicated. Key: \"{}\" Type: \"{}\"" + ).format(data["key"], data["type"]["name"])) def get_required(self, attr): output = {} for key in self.required_keys: if key not in attr: raise CustAttrException( - 'Key {} is required - please set'.format(key) + "BUG: Key \"{}\" is required".format(key) ) if attr['type'].lower() not in self.type_posibilities: @@ -593,17 +792,17 @@ class CustomAttributes(BaseAction): def get_optional(self, attr): output = {} - if 'group' in attr: - output['group'] = self.get_group(attr) - if 'default' in attr: - output['default'] = self.get_default(attr) + if "group" in attr: + output["group"] = self.get_group(attr) + if "default" in attr: + output["default"] = self.get_default(attr) roles_read = [] roles_write = [] - if 'read_security_roles' in output: - roles_read = attr['read_security_roles'] - if 'read_security_roles' in output: - roles_write = attr['write_security_roles'] + if "read_security_roles" in attr: + roles_read = attr["read_security_roles"] + if "write_security_roles" in attr: + roles_write = attr["write_security_roles"] output['read_security_roles'] = self.get_security_roles(roles_read) output['write_security_roles'] = self.get_security_roles(roles_write) diff --git a/pype/modules/ftrack/actions/action_delete_old_versions.py b/pype/modules/ftrack/actions/action_delete_old_versions.py index 46652b136a..6a4c5a0cae 100644 --- a/pype/modules/ftrack/actions/action_delete_old_versions.py +++ b/pype/modules/ftrack/actions/action_delete_old_versions.py @@ -105,11 +105,34 @@ class DeleteOldVersions(BaseAction): "value": False }) + items.append(self.splitter_item) + + items.append({ + "type": "label", + "value": ( + "This will NOT delete any files and only return the " + "total size of the files." + ) + }) + items.append({ + "type": "boolean", + "name": "only_calculate", + "label": "Only calculate size of files.", + "value": False + }) + return { "items": items, "title": self.inteface_title } + def sizeof_fmt(self, num, suffix='B'): + for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: + if abs(num) < 1024.0: + return "%3.1f%s%s" % (num, unit, suffix) + num /= 1024.0 + return "%.1f%s%s" % (num, 'Yi', suffix) + def launch(self, session, entities, event): values = event["data"].get("values") if not values: @@ -117,6 +140,7 @@ class DeleteOldVersions(BaseAction): versions_count = int(values["last_versions_count"]) force_to_remove = values["force_delete_publish_folder"] + only_calculate = values["only_calculate"] _val1 = "OFF" if force_to_remove: @@ -318,10 +342,29 @@ class DeleteOldVersions(BaseAction): "Folder does not exist. Deleting it's files skipped: {}" ).format(paths_msg)) + # Size of files. + size = 0 + + if only_calculate: + if force_to_remove: + size = self.delete_whole_dir_paths( + dir_paths.values(), delete=False + ) + else: + size = self.delete_only_repre_files( + dir_paths, file_paths_by_dir, delete=False + ) + + msg = "Total size of files: " + self.sizeof_fmt(size) + + self.log.warning(msg) + + return {"success": True, "message": msg} + if force_to_remove: - self.delete_whole_dir_paths(dir_paths.values()) + size = self.delete_whole_dir_paths(dir_paths.values()) else: - self.delete_only_repre_files(dir_paths, file_paths_by_dir) + size = self.delete_only_repre_files(dir_paths, file_paths_by_dir) mongo_changes_bulk = [] for version in versions: @@ -383,17 +426,31 @@ class DeleteOldVersions(BaseAction): "message": msg } - return True + msg = "Total size of files deleted: " + self.sizeof_fmt(size) + + self.log.warning(msg) + + return {"success": True, "message": msg} + + def delete_whole_dir_paths(self, dir_paths, delete=True): + size = 0 - def delete_whole_dir_paths(self, dir_paths): for dir_path in dir_paths: # Delete all files and fodlers in dir path for root, dirs, files in os.walk(dir_path, topdown=False): for name in files: - os.remove(os.path.join(root, name)) + file_path = os.path.join(root, name) + size += os.path.getsize(file_path) + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) for name in dirs: - os.rmdir(os.path.join(root, name)) + if delete: + os.rmdir(os.path.join(root, name)) + + if not delete: + continue # Delete even the folder and it's parents folders if they are empty while True: @@ -406,7 +463,11 @@ class DeleteOldVersions(BaseAction): os.rmdir(os.path.join(dir_path)) - def delete_only_repre_files(self, dir_paths, file_paths): + return size + + def delete_only_repre_files(self, dir_paths, file_paths, delete=True): + size = 0 + for dir_id, dir_path in dir_paths.items(): dir_files = os.listdir(dir_path) collections, remainders = clique.assemble(dir_files) @@ -420,8 +481,13 @@ class DeleteOldVersions(BaseAction): "File was not found: {}".format(file_path) ) continue - os.remove(file_path) - self.log.debug("Removed file: {}".format(file_path)) + + size += os.path.getsize(file_path) + + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) + remainders.remove(file_path_base) continue @@ -440,21 +506,34 @@ class DeleteOldVersions(BaseAction): final_col.head = os.path.join(dir_path, final_col.head) for _file_path in final_col: if os.path.exists(_file_path): - os.remove(_file_path) + + size += os.path.getsize(_file_path) + + if delete: + os.remove(_file_path) + self.log.debug( + "Removed file: {}".format(_file_path) + ) + _seq_path = final_col.format("{head}{padding}{tail}") self.log.debug("Removed files: {}".format(_seq_path)) collections.remove(final_col) elif os.path.exists(file_path): - os.remove(file_path) - self.log.debug("Removed file: {}".format(file_path)) + size += os.path.getsize(file_path) + if delete: + os.remove(file_path) + self.log.debug("Removed file: {}".format(file_path)) else: self.log.warning( "File was not found: {}".format(file_path) ) # Delete as much as possible parent folders + if not delete: + return size + for dir_path in dir_paths.values(): while True: if not os.path.exists(dir_path): @@ -467,6 +546,8 @@ class DeleteOldVersions(BaseAction): self.log.debug("Removed folder: {}".format(dir_path)) os.rmdir(dir_path) + return size + def path_from_represenation(self, representation, anatomy): try: template = representation["data"]["template"] diff --git a/pype/modules/ftrack/actions/action_prepare_project.py b/pype/modules/ftrack/actions/action_prepare_project.py index f51a9eb9a6..b3a2a20151 100644 --- a/pype/modules/ftrack/actions/action_prepare_project.py +++ b/pype/modules/ftrack/actions/action_prepare_project.py @@ -3,7 +3,7 @@ import json from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import config, Anatomy, project_overrides_dir_path -from pype.modules.ftrack.lib.avalon_sync import get_avalon_attr +from pype.modules.ftrack.lib.avalon_sync import get_pype_attr class PrepareProject(BaseAction): @@ -221,7 +221,7 @@ class PrepareProject(BaseAction): def _attributes_to_set(self, project_defaults): attributes_to_set = {} - cust_attrs, hier_cust_attrs = get_avalon_attr(self.session, True) + cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True) for attr in hier_cust_attrs: key = attr["key"] diff --git a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py index b399dab7ce..94ca503233 100644 --- a/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py +++ b/pype/modules/ftrack/actions/action_store_thumbnails_to_avalon.py @@ -8,7 +8,7 @@ from pype.modules.ftrack.lib import BaseAction, statics_icon from pype.api import Anatomy from pype.modules.ftrack.lib.io_nonsingleton import DbConnector -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY class StoreThumbnailsToAvalon(BaseAction): @@ -390,7 +390,7 @@ class StoreThumbnailsToAvalon(BaseAction): return output asset_ent = None - asset_mongo_id = parent["custom_attributes"].get(CustAttrIdKey) + asset_mongo_id = parent["custom_attributes"].get(CUST_ATTR_ID_KEY) if asset_mongo_id: try: asset_mongo_id = ObjectId(asset_mongo_id) diff --git a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py index 89bad52f29..ee82c9589d 100644 --- a/pype/modules/ftrack/events/event_del_avalon_id_from_new.py +++ b/pype/modules/ftrack/events/event_del_avalon_id_from_new.py @@ -1,5 +1,5 @@ from pype.modules.ftrack.lib import BaseEvent -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from pype.modules.ftrack.events.event_sync_to_avalon import SyncToAvalonEvent @@ -29,7 +29,7 @@ class DelAvalonIdFromNew(BaseEvent): elif ( entity.get('action', None) == 'update' and - CustAttrIdKey in entity['keys'] and + CUST_ATTR_ID_KEY in entity['keys'] and entity_id in created ): ftrack_entity = session.get( @@ -37,12 +37,9 @@ class DelAvalonIdFromNew(BaseEvent): entity_id ) - cust_attr = ftrack_entity['custom_attributes'][ - CustAttrIdKey - ] - - if cust_attr != '': - ftrack_entity['custom_attributes'][CustAttrIdKey] = '' + cust_attrs = ftrack_entity["custom_attributes"] + if cust_attrs[CUST_ATTR_ID_KEY]: + cust_attrs[CUST_ATTR_ID_KEY] = "" session.commit() except Exception: diff --git a/pype/modules/ftrack/events/event_sync_to_avalon.py b/pype/modules/ftrack/events/event_sync_to_avalon.py index 739ec69522..efcb74a608 100644 --- a/pype/modules/ftrack/events/event_sync_to_avalon.py +++ b/pype/modules/ftrack/events/event_sync_to_avalon.py @@ -14,7 +14,7 @@ from avalon import schema from pype.modules.ftrack.lib import avalon_sync from pype.modules.ftrack.lib.avalon_sync import ( - CustAttrIdKey, CustAttrAutoSync, EntitySchemas + CUST_ATTR_ID_KEY, CUST_ATTR_AUTO_SYNC, EntitySchemas ) import ftrack_api from pype.modules.ftrack import BaseEvent @@ -103,7 +103,7 @@ class SyncToAvalonEvent(BaseEvent): @property def avalon_cust_attrs(self): if self._avalon_cust_attrs is None: - self._avalon_cust_attrs = avalon_sync.get_avalon_attr( + self._avalon_cust_attrs = avalon_sync.get_pype_attr( self.process_session ) return self._avalon_cust_attrs @@ -220,7 +220,7 @@ class SyncToAvalonEvent(BaseEvent): def avalon_custom_attributes(self): """Return info about changeability of entity and it's parents.""" if self._avalon_custom_attributes is None: - self._avalon_custom_attributes = avalon_sync.get_avalon_attr( + self._avalon_custom_attributes = avalon_sync.get_pype_attr( self.process_session ) return self._avalon_custom_attributes @@ -557,10 +557,10 @@ class SyncToAvalonEvent(BaseEvent): continue changes = ent_info["changes"] - if CustAttrAutoSync not in changes: + if CUST_ATTR_AUTO_SYNC not in changes: continue - auto_sync = changes[CustAttrAutoSync]["new"] + auto_sync = changes[CUST_ATTR_AUTO_SYNC]["new"] if auto_sync == "1": # Trigger sync to avalon action if auto sync was turned on ft_project = self.cur_project @@ -593,16 +593,16 @@ class SyncToAvalonEvent(BaseEvent): ft_project = self.cur_project # Check if auto-sync custom attribute exists - if CustAttrAutoSync not in ft_project["custom_attributes"]: + if CUST_ATTR_AUTO_SYNC not in ft_project["custom_attributes"]: # TODO should we sent message to someone? self.log.error(( "Custom attribute \"{}\" is not created or user \"{}\" used" " for Event server don't have permissions to access it!" - ).format(CustAttrAutoSync, self.session.api_user)) + ).format(CUST_ATTR_AUTO_SYNC, self.session.api_user)) return True # Skip if auto-sync is not set - auto_sync = ft_project["custom_attributes"][CustAttrAutoSync] + auto_sync = ft_project["custom_attributes"][CUST_ATTR_AUTO_SYNC] if auto_sync is not True: return True @@ -844,7 +844,7 @@ class SyncToAvalonEvent(BaseEvent): new_entity["custom_attributes"][key] = val - new_entity["custom_attributes"][CustAttrIdKey] = ( + new_entity["custom_attributes"][CUST_ATTR_ID_KEY] = ( str(avalon_entity["_id"]) ) ent_path = self.get_ent_path(new_entity_id) @@ -1097,7 +1097,7 @@ class SyncToAvalonEvent(BaseEvent): continue final_entity["data"][key] = val - _mongo_id_str = cust_attrs.get(CustAttrIdKey) + _mongo_id_str = cust_attrs.get(CUST_ATTR_ID_KEY) if _mongo_id_str: try: _mongo_id = ObjectId(_mongo_id_str) @@ -1158,15 +1158,17 @@ class SyncToAvalonEvent(BaseEvent): self.log.debug("Entity was synchronized <{}>".format(ent_path)) mongo_id_str = str(mongo_id) - if mongo_id_str != ftrack_ent["custom_attributes"][CustAttrIdKey]: - ftrack_ent["custom_attributes"][CustAttrIdKey] = mongo_id_str + if mongo_id_str != ftrack_ent["custom_attributes"][CUST_ATTR_ID_KEY]: + ftrack_ent["custom_attributes"][CUST_ATTR_ID_KEY] = mongo_id_str try: self.process_session.commit() except Exception: self.process_session.rolback() # TODO logging # TODO report - error_msg = "Failed to store MongoID to entity's custom attribute" + error_msg = ( + "Failed to store MongoID to entity's custom attribute" + ) report_msg = ( "{}||SyncToAvalon action may solve this issue" ).format(error_msg) @@ -1245,7 +1247,7 @@ class SyncToAvalonEvent(BaseEvent): self.process_session, entity, hier_keys, defaults ) for key, val in hier_values.items(): - if key == CustAttrIdKey: + if key == CUST_ATTR_ID_KEY: continue output[key] = val @@ -1687,7 +1689,7 @@ class SyncToAvalonEvent(BaseEvent): if "_hierarchical" not in temp_dict: hier_mongo_id_configuration_id = None for attr in hier_attrs: - if attr["key"] == CustAttrIdKey: + if attr["key"] == CUST_ATTR_ID_KEY: hier_mongo_id_configuration_id = attr["id"] break temp_dict["_hierarchical"] = hier_mongo_id_configuration_id @@ -1704,7 +1706,7 @@ class SyncToAvalonEvent(BaseEvent): for attr in cust_attrs: key = attr["key"] - if key != CustAttrIdKey: + if key != CUST_ATTR_ID_KEY: continue if attr["entity_type"] != ent_info["entityType"]: diff --git a/pype/modules/ftrack/events/event_user_assigment.py b/pype/modules/ftrack/events/event_user_assigment.py index e198ced618..d1b3439c8f 100644 --- a/pype/modules/ftrack/events/event_user_assigment.py +++ b/pype/modules/ftrack/events/event_user_assigment.py @@ -3,7 +3,7 @@ import re import subprocess from pype.modules.ftrack import BaseEvent -from pype.modules.ftrack.lib.avalon_sync import CustAttrIdKey +from pype.modules.ftrack.lib.avalon_sync import CUST_ATTR_ID_KEY from pype.modules.ftrack.lib.io_nonsingleton import DbConnector from bson.objectid import ObjectId @@ -106,7 +106,7 @@ class UserAssigmentEvent(BaseEvent): self.db_con.Session['AVALON_PROJECT'] = task['project']['full_name'] avalon_entity = None - parent_id = parent['custom_attributes'].get(CustAttrIdKey) + parent_id = parent['custom_attributes'].get(CUST_ATTR_ID_KEY) if parent_id: parent_id = ObjectId(parent_id) avalon_entity = self.db_con.find_one({ diff --git a/pype/modules/ftrack/lib/__init__.py b/pype/modules/ftrack/lib/__init__.py index df546ab725..d8e9c7a11c 100644 --- a/pype/modules/ftrack/lib/__init__.py +++ b/pype/modules/ftrack/lib/__init__.py @@ -5,7 +5,7 @@ from .ftrack_event_handler import BaseEvent from .ftrack_action_handler import BaseAction, statics_icon from .ftrack_app_handler import AppAction -__all__ = [ +__all__ = ( "avalon_sync", "credentials", "BaseHandler", @@ -13,4 +13,4 @@ __all__ = [ "BaseAction", "statics_icon", "AppAction" -] +) diff --git a/pype/modules/ftrack/lib/avalon_sync.py b/pype/modules/ftrack/lib/avalon_sync.py index 885b9d25cc..4bab1676d4 100644 --- a/pype/modules/ftrack/lib/avalon_sync.py +++ b/pype/modules/ftrack/lib/avalon_sync.py @@ -1,6 +1,7 @@ import os import re import queue +import json import collections import copy @@ -27,9 +28,21 @@ EntitySchemas = { "config": "avalon-core:config-1.0" } +# Group name of custom attributes +CUST_ATTR_GROUP = "pype" + # name of Custom attribute that stores mongo_id from avalon db -CustAttrIdKey = "avalon_mongo_id" -CustAttrAutoSync = "avalon_auto_sync" +CUST_ATTR_ID_KEY = "avalon_mongo_id" +CUST_ATTR_AUTO_SYNC = "avalon_auto_sync" + + +def default_custom_attributes_definition(): + json_file_path = os.path.join( + os.path.dirname(__file__), "custom_attributes.json" + ) + with open(json_file_path, "r") as json_stream: + data = json.load(json_stream) + return data def check_regex(name, entity_type, in_schema=None, schema_patterns=None): @@ -51,10 +64,11 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None): if not schema_obj: name_pattern = default_pattern else: - name_pattern = schema_obj.get( - "properties", {}).get( - "name", {}).get( - "pattern", default_pattern + name_pattern = ( + schema_obj + .get("properties", {}) + .get("name", {}) + .get("pattern", default_pattern) ) if schema_patterns is not None: schema_patterns[schema_name] = name_pattern @@ -64,9 +78,10 @@ def check_regex(name, entity_type, in_schema=None, schema_patterns=None): return False -def get_avalon_attr(session, split_hierarchical=True): +def get_pype_attr(session, split_hierarchical=True): custom_attributes = [] hier_custom_attributes = [] + # TODO remove deprecated "avalon" group from query cust_attrs_query = ( "select id, entity_type, object_type_id, is_hierarchical, default" " from CustomAttributeConfiguration" @@ -322,12 +337,12 @@ class SyncEntitiesFactory: "*** Synchronization initialization started <{}>." ).format(project_full_name)) # Check if `avalon_mongo_id` custom attribute exist or is accessible - if CustAttrIdKey not in ft_project["custom_attributes"]: + if CUST_ATTR_ID_KEY not in ft_project["custom_attributes"]: items = [] items.append({ "type": "label", "value": "# Can't access Custom attribute <{}>".format( - CustAttrIdKey + CUST_ATTR_ID_KEY ) }) items.append({ @@ -687,7 +702,7 @@ class SyncEntitiesFactory: def set_cutom_attributes(self): self.log.debug("* Preparing custom attributes") # Get custom attributes and values - custom_attrs, hier_attrs = get_avalon_attr(self.session) + custom_attrs, hier_attrs = get_pype_attr(self.session) ent_types = self.session.query("select id, name from ObjectType").all() ent_types_by_name = { ent_type["name"]: ent_type["id"] for ent_type in ent_types @@ -904,7 +919,7 @@ class SyncEntitiesFactory: project_values[key] = value for key in avalon_hier: - if key == CustAttrIdKey: + if key == CUST_ATTR_ID_KEY: continue value = self.entities_dict[top_id]["avalon_attrs"][key] if value is not None: @@ -1058,7 +1073,7 @@ class SyncEntitiesFactory: same_mongo_id = [] all_mongo_ids = {} for ftrack_id, entity_dict in self.entities_dict.items(): - mongo_id = entity_dict["avalon_attrs"].get(CustAttrIdKey) + mongo_id = entity_dict["avalon_attrs"].get(CUST_ATTR_ID_KEY) if not mongo_id: continue if mongo_id in all_mongo_ids: @@ -1089,7 +1104,7 @@ class SyncEntitiesFactory: entity_dict = self.entities_dict[ftrack_id] ent_path = self.get_ent_path(ftrack_id) - mongo_id = entity_dict["avalon_attrs"].get(CustAttrIdKey) + mongo_id = entity_dict["avalon_attrs"].get(CUST_ATTR_ID_KEY) av_ent_by_mongo_id = self.avalon_ents_by_id.get(mongo_id) if av_ent_by_mongo_id: av_ent_ftrack_id = av_ent_by_mongo_id.get("data", {}).get( @@ -1110,7 +1125,9 @@ class SyncEntitiesFactory: continue _entity_dict = self.entities_dict[_ftrack_id] - _mongo_id = _entity_dict["avalon_attrs"][CustAttrIdKey] + _mongo_id = ( + _entity_dict["avalon_attrs"][CUST_ATTR_ID_KEY] + ) _av_ent_by_mongo_id = self.avalon_ents_by_id.get( _mongo_id ) @@ -1503,11 +1520,11 @@ class SyncEntitiesFactory: avalon_attrs = self.entities_dict[ftrack_id]["avalon_attrs"] if ( - CustAttrIdKey not in avalon_attrs or - avalon_attrs[CustAttrIdKey] != avalon_id + CUST_ATTR_ID_KEY not in avalon_attrs or + avalon_attrs[CUST_ATTR_ID_KEY] != avalon_id ): configuration_id = self.entities_dict[ftrack_id][ - "avalon_attrs_id"][CustAttrIdKey] + "avalon_attrs_id"][CUST_ATTR_ID_KEY] _entity_key = collections.OrderedDict({ "configuration_id": configuration_id, @@ -1587,7 +1604,7 @@ class SyncEntitiesFactory: # avalon_archived_by_id avalon_archived_by_name current_id = ( - entity_dict["avalon_attrs"].get(CustAttrIdKey) or "" + entity_dict["avalon_attrs"].get(CUST_ATTR_ID_KEY) or "" ).strip() mongo_id = current_id name = entity_dict["name"] @@ -1623,14 +1640,14 @@ class SyncEntitiesFactory: if current_id != new_id_str: # store mongo id to ftrack entity configuration_id = self.hier_cust_attr_ids_by_key.get( - CustAttrIdKey + CUST_ATTR_ID_KEY ) if not configuration_id: - # NOTE this is for cases when CustAttrIdKey key is not + # NOTE this is for cases when CUST_ATTR_ID_KEY key is not # hierarchical custom attribute but per entity type configuration_id = self.entities_dict[ftrack_id][ "avalon_attrs_id" - ][CustAttrIdKey] + ][CUST_ATTR_ID_KEY] _entity_key = collections.OrderedDict({ "configuration_id": configuration_id, @@ -1739,7 +1756,7 @@ class SyncEntitiesFactory: project_item = self.entities_dict[self.ft_project_id]["final_entity"] mongo_id = ( self.entities_dict[self.ft_project_id]["avalon_attrs"].get( - CustAttrIdKey + CUST_ATTR_ID_KEY ) or "" ).strip() @@ -1770,7 +1787,7 @@ class SyncEntitiesFactory: # store mongo id to ftrack entity entity = self.entities_dict[self.ft_project_id]["entity"] - entity["custom_attributes"][CustAttrIdKey] = str(new_id) + entity["custom_attributes"][CUST_ATTR_ID_KEY] = str(new_id) def _bubble_changeability(self, unchangeable_ids): unchangeable_queue = queue.Queue() @@ -2151,7 +2168,7 @@ class SyncEntitiesFactory: if new_entity_id not in p_chilren: self.entities_dict[parent_id]["children"].append(new_entity_id) - cust_attr, hier_attrs = get_avalon_attr(self.session) + cust_attr, hier_attrs = get_pype_attr(self.session) for _attr in cust_attr: key = _attr["key"] if key not in av_entity["data"]: @@ -2167,7 +2184,7 @@ class SyncEntitiesFactory: new_entity["custom_attributes"][key] = value av_entity_id = str(av_entity["_id"]) - new_entity["custom_attributes"][CustAttrIdKey] = av_entity_id + new_entity["custom_attributes"][CUST_ATTR_ID_KEY] = av_entity_id self.ftrack_avalon_mapper[new_entity_id] = av_entity_id self.avalon_ftrack_mapper[av_entity_id] = new_entity_id diff --git a/pype/modules/ftrack/lib/custom_attributes.json b/pype/modules/ftrack/lib/custom_attributes.json new file mode 100644 index 0000000000..17ff6691d3 --- /dev/null +++ b/pype/modules/ftrack/lib/custom_attributes.json @@ -0,0 +1,60 @@ +{ + "show": { + "avalon_auto_sync": { + "label": "Avalon auto-sync", + "type": "boolean", + "write_security_role": ["API", "Administrator"], + "read_security_role": ["API", "Administrator"] + }, + "library_project": { + "label": "Library Project", + "type": "boolean", + "write_security_role": ["API", "Administrator"], + "read_security_role": ["API", "Administrator"] + } + }, + "is_hierarchical": { + "fps": { + "label": "FPS", + "type": "number", + "config": {"isdecimal": true} + }, + "clipIn": { + "label": "Clip in", + "type": "number" + }, + "clipOut": { + "label": "Clip out", + "type": "number" + }, + "frameStart": { + "label": "Frame start", + "type": "number" + }, + "frameEnd": { + "label": "Frame end", + "type": "number" + }, + "resolutionWidth": { + "label": "Resolution Width", + "type": "number" + }, + "resolutionHeight": { + "label": "Resolution Height", + "type": "number" + }, + "pixelAspect": { + "label": "Pixel aspect", + "type": "number", + "config": {"isdecimal": true} + }, + "handleStart": { + "label": "Frame handles start", + "type": "number" + }, + "handleEnd": { + "label": "Frame handles end", + "type": "number" + } + } +} diff --git a/pype/modules/websocket_server/__init__.py b/pype/modules/websocket_server/__init__.py new file mode 100644 index 0000000000..eb5a0d9f27 --- /dev/null +++ b/pype/modules/websocket_server/__init__.py @@ -0,0 +1,5 @@ +from .websocket_server import WebSocketServer + + +def tray_init(tray_widget, main_widget): + return WebSocketServer() diff --git a/pype/modules/websocket_server/hosts/__init__.py b/pype/modules/websocket_server/hosts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pype/modules/websocket_server/hosts/external_app_1.py b/pype/modules/websocket_server/hosts/external_app_1.py new file mode 100644 index 0000000000..9352787175 --- /dev/null +++ b/pype/modules/websocket_server/hosts/external_app_1.py @@ -0,0 +1,47 @@ +import asyncio + +from pype.api import Logger +from wsrpc_aiohttp import WebSocketRoute + +log = Logger().get_logger("WebsocketServer") + + +class ExternalApp1(WebSocketRoute): + """ + One route, mimicking external application (like Harmony, etc). + All functions could be called from client. + 'do_notify' function calls function on the client - mimicking + notification after long running job on the server or similar + """ + + def init(self, **kwargs): + # Python __init__ must be return "self". + # This method might return anything. + log.debug("someone called ExternalApp1 route") + return kwargs + + async def server_function_one(self): + log.info('In function one') + + async def server_function_two(self): + log.info('In function two') + return 'function two' + + async def server_function_three(self): + log.info('In function three') + asyncio.ensure_future(self.do_notify()) + return '{"message":"function tree"}' + + async def server_function_four(self, *args, **kwargs): + log.info('In function four args {} kwargs {}'.format(args, kwargs)) + ret = dict(**kwargs) + ret["message"] = "function four received arguments" + return str(ret) + + # This method calls function on the client side + async def do_notify(self): + import time + time.sleep(5) + log.info('Calling function on server after delay') + awesome = 'Somebody server_function_three method!' + await self.socket.call('notify', result=awesome) diff --git a/pype/modules/websocket_server/test_client/wsrpc_client.html b/pype/modules/websocket_server/test_client/wsrpc_client.html new file mode 100644 index 0000000000..9c3f469aca --- /dev/null +++ b/pype/modules/websocket_server/test_client/wsrpc_client.html @@ -0,0 +1,179 @@ + + +
+ +