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)