Skip to content

To create a new way to use sgtk tempalte path without sgtk ​

First, we need to create a loader base class ​

py
class BaseLoader(dict):
    """ Base class for all loaders."""
    def __init__(self, *args, **kwargs):
        super(BaseLoader, self).__init__(*args, **kwargs)

    @property
    def keys(self):
        """ Return the keys of the dict."""
        return self.__dict__.keys()

    def load(self, *args, **kwargs):
        """ load method must be implemented."""
        raise NotImplementedError("load method must be implemented")

    def to_template_path_list(self):
        """ Convert the dict to a list of template path, used by sgtk."""
        raise NotImplementedError("to_template_path_list method must be implemented")

    def to_template_path_dict(self):
        """ Convert the dict to a dict of template path, used by sgtk."""
        raise NotImplementedError("to_template_path_dict method must be implemented")

    def __repr__(self):
        return str(self.__dict__)

Then we can create a yaml loader class, because tk template use .yml file to store each tempalte path data ​

py
class YamlLoader(BaseLoader):
    """ Yaml Loader class."""
    def __init__(self, *args, **kwargs):
        super(BaseLoader, self).__init__(*args, **kwargs)

    def load(self, file_path: str):
        """ Load yaml file to current dict instance."""

        try:
            with open(file_path, 'r') as yaml_content:
                self.__dict__.update(yaml.safe_load(yaml_content))
        except FileNotFoundError as e:
            raise FileNotFoundError('file_path does not exist: {}'.format(e))

        except Exception as e:
            raise Exception('Error while loading yaml file: {}'.format(e))

    def extract_fields(self):
        """ Extract all fields from the template path."""

        if not self.paths:
            return []

        return self._regex_fields.findall(self.paths)

    def to_template_path_list(self):
        """ Convert the dict to a list of template path, used by sgtk."""
        if not self.paths:
            return []

        return [TemplatePathBase(val) for key, val in self.paths.items()]

    def to_template_path_dict(self):
        """ Convert the dict to a dict of template path, used by sgtk."""
        if not self.paths:
            return {}

        for key, val in self.paths.items():
            self.paths[key] = TemplatePathBase(val)

        return self.paths

TKYamlLoader ​

We can now create our loader specific to sgtk template path yaml file

py
class ShotgunYamlLoader(YamlLoader):
    def __init__(self, *args, **kwargs):
        super(ShotgunYamlLoader, self).__init__(*args, **kwargs)

    @property
        def _shiny_regex(self):
        """ This regex is used to decompose the template path into groups.
        Each group is a field of the template path, and each field is a regex.

        Example: this template path: shoot/{ShootDay}/{MocapSetup}/data/{MocapSetup}[.{name}].v{version}[.{desc}].{ext}
        will be decomposed into this list of groups:
            - Group str: shoot
            - Group sep: /
            - Group key: {ShootDay}
            - Group sep: /
            - Group key: {MocapSetup}
            - Group sep: /
            - Group str: data
            - Group sep: /
            - Group key: {MocapSetup}
            - Group oneopt: [.{name}]
            - Group str: .
            - Group str: v
            - Group key: {version}
            - Group oneopt: [.{desc}]
            - Group str: .
            - Group key: {ext}
        """
        return re.compile(r'(?P<oneopt>\[[^\[\]]+\])|(?P<mulopt>\[.*\])|((?P<key>{\w+})|(?P<str>\.|\w+)|(?P<sep>[^\[\]]))')

    @property
    def _shiny_groups(self):
        return self._shiny_regex.findall(self.value)

    @property
    def _key_field_regex(self):
        """ Return the regex for the key field of the template path."""
        return re.compile(r'{(\w+)}')

    @property
    def _optional_fields_regex(self):
        """ Return optional fields of the template path."""
        return re.compile(r'\[[^\[\]]+\]')

    @property
    def _regex_fields(self):
        """ Return all fields key of the template path."""
        return re.compile(r'{(\w+)}')

    @property
    def _regex_optional_fields(self):
        """ Return all optional fields key of the template path."""
        return re.compile(r'(\[.+?(?=\]).)')

    @property
    def _regex_variables(self):
        return re.compile(r'(@\w*[^_\/.\]\[\\\}\{])')

    @property
    def tk_keys(self):
        return self.__dict__.get('keys')

    @property
    def paths(self):
        return self.__dict__.get('paths')

    @property
    def strings(self):
        return self.__dict__.get('strings')

    @property
    def fields(self):
        return self.get('fields')

    @property
    def final_data(self):
        """ Return temporary data structure matching path template per fields."""
        return {
            key: {
                'value': value,
                'fields': self.fields.get(key)
            }
            for key, value in self.paths.items()
        }

    def _clean_dictionary(self):
        """ Clean the dictionary by removing all None values."""

        temp_dict = self.paths.copy()
        for key, val in temp_dict.items():

            if isinstance(val, str):
                continue

            if isinstance(val, dict) and 'definition' in val.keys():
                self.paths[key] = val['definition']
                continue

            del self.paths[key]

    def _extract_fields(self):
        """ Extract all fields from the shotgun yaml file."""

        fields = {}

        for key in self.paths.keys():
            template_path = self.paths.get(key)
            if not isinstance(template_path, str):
                continue

            # use regex to extract fields from the path
            groups = list(self._shiny_regex.finditer(template_path))

            if not groups:
                continue

            #TODO create function to convert template path fields to custom object fields to work with
            by_regex_group_name = [{
                    'name': name,
                    'value': value
                }
                for re_object in groups
                for name, value in re_object.groupdict().items() if value
            ]
            print('original: {}'.format(template_path))

            ## todo F IS My debug stuff -> to remove
            f = self._fields_to_object_fields(by_regex_group_name)
            f = [str(field) for field in f if field]
            print('result: {}'.format(''.join(f)))
            print('============================================')

            fields[key] = f
        return fields
    def _find_all_variables_keys(self, path_token) -> str:
        """ Find all @variables by their value for each path_token.
        We need to use recursion to find all variables keys because a path_token can also contain a path_token.

        :param path_token: The path_token to parse, like @project_root/something/@project_name
        """
        dict_paths = self.paths
        dict_strings = self.strings

        if not isinstance(path_token, str):
            return path_token

        if not '@' in path_token:
            return path_token

        matches = self._regex_variables.findall(path_token)

        for match in matches:
            rk = match.replace('@', '')
            clean_path = path_token.replace(match, '')

            value = None
            if dict_paths.get(rk):
                value = dict_paths.get(rk)

            if dict_strings.get(rk):
                value = dict_strings.get(rk)

            if not value:
                continue

            # if value start with /, and clean_path end with /, we need to remove one of them
            if clean_path.startswith('/') and value[-1] == '/':
                # todo flag not valid
                value = value[:-1]

            value = path_token.replace(match, value)
            return self._find_all_variables_keys(value)

    @property
    def _tk_type_to_field(self):
        return {
            'str': TKStrField,
            'int': TKIntField,
            'sequence': TKStrField,
            'key': TKKeyField,
            'sep': TKSeparatorField,
        }

    @property
    def _key_mapping_regex(self):
        return {
            'str': re.compile(r'(.+)'),
            'key': re.compile(r'(\w+)'),
            'sep': re.compile(r'(.)'),
        }

    def _extract_field_key(self, template_field_type, template_field_value):
        """ Extract the key from the template field."""
        regex = self._key_mapping_regex.get(template_field_type)

        if not regex:
            return None

        if not regex.findall(template_field_value):
            return None

        key = regex.findall(template_field_value)[0]
        return self.tk_keys.get(key)

    def _extract_field_value(self, template_field_type, template_field_value):
        """ Extract the value from the template field."""
        regex = self._key_mapping_regex.get(template_field_type)

        if not regex:
            return template_field_value

        if not regex.findall(template_field_value):
            return template_field_value

        return regex.findall(template_field_value)[0]

    def create_field(self, segment_type, segment_value):
        # These static field (type and value) are given by the TK yaml file
        basic_value = self._extract_field_value(segment_type, segment_value)
        tk_key_data = self._extract_field_key(segment_type, segment_value)

        # if type of segment is not or not tk_key_data attached to this key we juste return the value as plain text in path
        if segment_type != 'key' or not tk_key_data:
            return self._tk_type_to_field[segment_type](value=basic_value)

        tk_type = tk_key_data.get('type')
        if tk_type not in self._tk_type_to_field.keys():
            raise Exception('The TK type {} is not supported.'.format(tk_type))

        return self._tk_type_to_field[tk_type](**tk_key_data, token_value=segment_value)

    def _fields_to_object_fields(self, groups=None, fields=None):
        """ Convert fields to object fields."""

        # End condition
        if not groups:
            return fields

        # if this is the first call, init fields
        fields = fields or []

        # take first group
        group = groups.pop(0)

        # extract regex group name and result
        group_type = group.get('name')
        group_value = group.get('value')

        # to manage nested optional fields
        if group_type in ['oneopt', 'mulopt']:

            optional_group = list(self._shiny_regex.finditer(group_value[1:-1]))
            by_regex_group_name = [
                {'name': name, 'value': value}
                for re_object in optional_group
                for name, value in re_object.groupdict().items() if value
            ]

            nested_fields = self._fields_to_object_fields(by_regex_group_name)
            final_optional_field = TKOptionalField(token_value=group_value, other_fields=nested_fields)
            fields.append(final_optional_field)

        else:
            basic_field = self.create_field(segment_type=group_type, segment_value=group_value)
            fields.append(basic_field)


        return self._fields_to_object_fields(groups, fields)


    def _parse_shotgun_yaml(self):
        """ Parse shotgun yaml file to find all variables keys"""
        if not 'paths' in self.keys:
            return
        dict_paths = self.paths
        # find all regex matches
        for key, val in dict_paths.items():
            self.paths[key] = self._find_all_variables_keys(val)

    def load(self, file_path: str) -> None:
        """ Load method, to get yaml as dict and parse it to find all variables keys, used by sgtk.

        :param file_path: The path to the yaml file.
        """

        ssa = time.time()
        super(ShotgunYamlLoader, self).load(file_path)
        self._clean_dictionary()
        self._parse_shotgun_yaml()

        self['fields'] = self._extract_fields()
        print('time: {}'.format(time.time() - ssa))

    def to_template_path_list(self):
        """ Convert the dict to a list of template path, matching sgtk usage."""
        if not self.paths:
            return []

        return [
            TemplatePathBase(
                value=value['value'],
                fields=value['fields']
            ) for key, value in self.final_data.items()
        ]

They are many recusrive things:

  • _find_all_variables_keys are used to convert all @variables to their value
  • _fields_to_object_fields are used to convert all regex groups to real object fields

Object fields ​

Because we have speak about object fields but nothing has been done, let's do it now.

First, base class ​

python
class BaseField(dict):
    """ Base class for all template path fields."""

    def __init__(self, **kwargs):
        super(BaseField, self).__init__()
        self.update(kwargs)

    @property
    def type(self):
        """ Return the type of this template path field."""
        return self.get('type')

    @property
    def default_value(self):
        """ Return the default value of this template path field."""
        return self.get('default_value')

    @property
    def value(self):
        """ Return the value of this template path field."""
        return self.get('value', self.default_value)

    @property
    def _regex(self):
        """ Return the regex of this template path field."""
        return self.get('regex')

    @property
    def regex(self):
        """ Return the regex of this template path field."""
        return self._regex or r'(\w+)'

    def __str__(self):
        return self.regex

Then base class for all tk fields ​

python

class TKField(BaseField):

    def __init__(self, *args, **kwargs):
        super(TKField, self).__init__(**kwargs)

    @property
    def value(self):
        return self.get('value')

    @property
    def format_spec(self):
        """ Return the format spec of the template path."""
        return self.get('format_spec')

    @property
    def _regex(self):
        """ Return the regex of the template path."""
        # TODO override regex field to represent the integer using the format_spec
        if not self.format_spec:
            return None

        return r'(\d{{{}}})'.format(self.format_spec)

    @property
    def alias(self):
        """ Return the alias of the template path.

        Do Not use!
        """
        return self.get('alias')

Class for str field ​

python


class TKStrField(TKField):
    """ TKStrField class represents a string field in a template path."""

    def __init__(self, **kwargs):
        super(TKStrField, self).__init__(**kwargs)
        self.token_value = kwargs.get('token_value')
    @property
    def filter_by(self):
        """ Return the filter type of the template path."""
        return self.get('filter_by')

    @property
    def choices(self):
        """ Return the choices of the template path."""
        return self.get('choices')

    @property
    def _regex(self):
        return self._clean_filter_by() or self._compute_regex_for_choices() or self.value

    def _clean_filter_by(self):
        """ Clean the filter_by field of the template path."""
        if not self.filter_by:
            return None

        clean_regex = self.filter_by

        if self.filter_by[0] == '^':
            clean_regex = clean_regex[1:]

        if self.filter_by[-1] == '$':
            clean_regex = clean_regex[:-1]
        return '({})'.format(clean_regex)

    def _compute_regex_for_choices(self):
        """ Compute the regex for the choices of the template path."""
        if not self.choices:
            return None

        regex = ''
        if isinstance(self.choices, list):
            regex = '|'.join(self.choices)

        if isinstance(self.choices, dict):
            regex = '|'.join([k for k, v in self.choices.items()])

        return '({})'.format(regex)

Class for separator field ​

python
class TKSeparatorField(TKStrField):
    def __init__(self, **kwargs):
        super(TKSeparatorField, self).__init__(**kwargs)

    @property
    def _regex(self):
        return r'\{}'.format(self.value)

Class for optional field ​

python
class TKOptionalField(TKStrField):
    def __init__(self, other_fields: list[TKField], **kwargs):
        super(TKOptionalField, self).__init__(**kwargs)
        self.other_fields = other_fields

    @property
    def _regex(self):
        # get all inter values
        inter_values = ''.join([a.regex for a in self.other_fields])
        return r'({})?'.format(inter_values)