Source code for ftrack_api.structure.standard

# :coding: utf-8
# :copyright: Copyright (c) 2015 ftrack

from builtins import str
import os
import re
import unicodedata

import ftrack_api.symbol
import ftrack_api.structure.base


[docs]class StandardStructure(ftrack_api.structure.base.Structure): '''Project hierarchy based structure that only supports Components. The resource identifier is generated from the project code, the name of objects in the project structure, asset name and version number:: my_project/folder_a/folder_b/asset_name/v003 If the component is a `FileComponent` then the name of the component and the file type are used as filename in the resource_identifier:: my_project/folder_a/folder_b/asset_name/v003/foo.jpg If the component is a `SequenceComponent` then a sequence expression, `%04d`, is used. E.g. a component with the name `foo` yields:: my_project/folder_a/folder_b/asset_name/v003/foo.%04d.jpg For the member components their index in the sequence is used:: my_project/folder_a/folder_b/asset_name/v003/foo.0042.jpg The name of the component is added to the resource identifier if the component is a `ContainerComponent`. E.g. a container component with the name `bar` yields:: my_project/folder_a/folder_b/asset_name/v003/bar For a member of that container the file name is based on the component name and file type:: my_project/folder_a/folder_b/asset_name/v003/bar/baz.pdf '''
[docs] def __init__( self, project_versions_prefix=None, illegal_character_substitute='_' ): '''Initialise structure. If *project_versions_prefix* is defined, insert after the project code for versions published directly under the project:: my_project/<project_versions_prefix>/v001/foo.jpg Replace illegal characters with *illegal_character_substitute* if defined. .. note:: Nested component containers/sequences are not supported. ''' super(StandardStructure, self).__init__() self.project_versions_prefix = project_versions_prefix self.illegal_character_substitute = illegal_character_substitute
def _get_parts(self, entity): '''Return resource identifier parts from *entity*.''' session = entity.session version = entity['version'] if version is ftrack_api.symbol.NOT_SET and entity['version_id']: version = session.get('AssetVersion', entity['version_id']) error_message = ( 'Component {0!r} must be attached to a committed ' 'version and a committed asset with a parent context.'.format( entity ) ) if ( version is ftrack_api.symbol.NOT_SET or version in session.created ): raise ftrack_api.exception.StructureError(error_message) link = version['link'] if not link: raise ftrack_api.exception.StructureError(error_message) structure_names = [ item['name'] for item in link[1:-1] ] project_id = link[0]['id'] project = session.get('Project', project_id) asset = version['asset'] version_number = self._format_version(version['version']) parts = [] parts.append(project['name']) if structure_names: parts.extend(structure_names) elif self.project_versions_prefix: # Add *project_versions_prefix* if configured and the version is # published directly under the project. parts.append(self.project_versions_prefix) parts.append(asset['name']) parts.append(version_number) return [self.sanitise_for_filesystem(part) for part in parts] def _format_version(self, number): '''Return a formatted string representing version *number*.''' return 'v{0:03d}'.format(number)
[docs] def sanitise_for_filesystem(self, value): '''Return *value* with illegal filesystem characters replaced. An illegal character is one that is not typically valid for filesystem usage, such as non ascii characters, or can be awkward to use in a filesystem, such as spaces. Replace these characters with the character specified by *illegal_character_substitute* on initialisation. If no character was specified as substitute then return *value* unmodified. ''' if self.illegal_character_substitute is None: return value value = unicodedata.normalize('NFKD', str(value)).encode('ascii', 'ignore') value = re.sub('[^\w\.-]', self.illegal_character_substitute, value.decode('utf-8')) return str(value.strip().lower())
[docs] def get_resource_identifier(self, entity, context=None): '''Return a resource identifier for supplied *entity*. *context* can be a mapping that supplies additional information, but is unused in this implementation. Raise a :py:exc:`ftrack_api.exeption.StructureError` if *entity* is not attached to a committed version and a committed asset with a parent context. ''' if entity.entity_type in ('FileComponent',): container = entity['container'] if container: # Get resource identifier for container. container_path = self.get_resource_identifier(container) if container.entity_type in ('SequenceComponent',): # Strip the sequence component expression from the parent # container and back the correct filename, i.e. # /sequence/component/sequence_component_name.0012.exr. name = '{0}.{1}{2}'.format( container['name'], entity['name'], entity['file_type'] ) parts = [ os.path.dirname(container_path), self.sanitise_for_filesystem(name) ] else: # Container is not a sequence component so add it as a # normal component inside the container. name = entity['name'] + entity['file_type'] parts = [ container_path, self.sanitise_for_filesystem(name) ] else: # File component does not have a container, construct name from # component name and file type. parts = self._get_parts(entity) name = entity['name'] + entity['file_type'] parts.append(self.sanitise_for_filesystem(name)) elif entity.entity_type in ('SequenceComponent',): # Create sequence expression for the sequence component and add it # to the parts. parts = self._get_parts(entity) sequence_expression = self._get_sequence_expression(entity) parts.append( '{0}.{1}{2}'.format( self.sanitise_for_filesystem(entity['name']), sequence_expression, self.sanitise_for_filesystem(entity['file_type']) ) ) elif entity.entity_type in ('ContainerComponent',): # Add the name of the container to the resource identifier parts. parts = self._get_parts(entity) parts.append(self.sanitise_for_filesystem(entity['name'])) else: raise NotImplementedError( 'Cannot generate resource identifier for unsupported ' 'entity {0!r}'.format(entity) ) return self.path_separator.join(parts)