# :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)