Coverage for /home/ubuntu/hidebound/python/hidebound/core/specification_base.py: 100%
128 statements
« prev ^ index » next coverage.py v7.5.4, created at 2024-07-05 23:50 +0000
« prev ^ index » next coverage.py v7.5.4, created at 2024-07-05 23:50 +0000
1from typing import Any, Dict, List, Optional, Tuple, Union # noqa F401
3from pathlib import Path
4import uuid
6from pyparsing import ParseException
7from schematics import Model
8from schematics.exceptions import ValidationError
9from schematics.types import IntType, ListType, StringType
11from hidebound.core.parser import AssetNameParser
12import hidebound.core.tools as hbt
13import hidebound.core.validators as vd
14# ------------------------------------------------------------------------------
17'''
18Contains the abstract base classes for all Hidebound specifications.
19'''
22_INDICATOR_LUT = dict(
23 project=AssetNameParser.PROJECT_INDICATOR,
24 specification=AssetNameParser.SPECIFICATION_INDICATOR,
25 descriptor=AssetNameParser.DESCRIPTOR_INDICATOR,
26 version=AssetNameParser.VERSION_INDICATOR,
27 coordinate=AssetNameParser.COORDINATE_INDICATOR,
28 frame=AssetNameParser.FRAME_INDICATOR,
29 extension=AssetNameParser.EXTENSION_INDICATOR,
30)
33class SpecificationBase(Model):
34 '''
35 The base class for all Hidebound specifications.
37 Attributes:
38 asset_type (str): Type of asset. Options include: file, sequence, complex.
39 filename_fields (list[str]): List of fields found in the asset filenames.
40 asset_name_fields (list[str]): List of fields found in the asset name.
41 project (str): Project name.
42 descriptor (str): Asset descriptor.
43 version (int): Asset version.
44 extension (str): File extension.
45 '''
46 asset_type = 'specification' # type: str
47 filename_fields = [
48 'project', 'specification', 'descriptor', 'version', 'extension'
49 ] # type: List[str]
50 asset_name_fields = ['project', 'specification', 'descriptor', 'version'] # type: List[str]
51 project = ListType(
52 StringType(), required=False, validators=[vd.is_project]
53 ) # type: ListType
54 specification = ListType(StringType()) # type: ListType
55 descriptor = ListType(
56 StringType(), required=False, validators=[vd.is_descriptor]
57 ) # type: ListType
58 version = ListType(
59 IntType(), required=False, validators=[vd.is_version]
60 ) # type: ListType
61 extension = ListType(
62 StringType(), required=True, validators=[vd.is_extension]
63 ) # type: ListType
64 file_traits = {} # type: Dict[str, Any]
66 def __init__(self, data={}):
67 # type: (Optional[Dict[str, Any]]) -> None
68 '''
69 Returns a new specification instance.
71 Args:
72 data (dict, optional): Dictionary of asset data.
73 '''
74 super().__init__(raw_data=data)
76 def get_asset_name(self, filepath):
77 # type: (Union[str, Path]) -> str
78 '''
79 Returns the expected asset name give a filepath.
81 Args:
82 filepath (str or Path): filepath to asset file.
84 Returns:
85 str: Asset name.
86 '''
87 filepath = Path(filepath)
88 data = AssetNameParser(self.filename_fields).parse(filepath.name) # type: dict
89 output = AssetNameParser(self.asset_name_fields).to_string(data) # type: str
90 return output
92 def get_asset_path(self, filepath):
93 # type: (Union[str, Path]) -> Path
94 '''
95 Returns the expected asset path given a filepath.
97 Args:
98 filepath (str or Path): filepath to asset file.
100 Raises:
101 NotImplementedError: If method not defined in subclass.
103 Returns:
104 Path: Asset path.
105 '''
106 msg = 'Method must be implemented in subclasses of SpecificationBase.'
107 raise NotImplementedError(msg)
109 def get_asset_id(self, filepath):
110 # type: (Union[str, Path]) -> str
111 '''
112 Returns a hash UUID of the asset directory or file, depending of asset
113 type.
115 Args:
116 filepath (str or Path): filepath to asset file.
118 Returns:
119 str: Asset id.
120 '''
121 return str(uuid.uuid3(
122 uuid.NAMESPACE_URL, self.get_asset_path(filepath).as_posix()
123 ))
125 def validate_filepath(self, filepath):
126 # type: (Union[str, Path]) -> None
127 '''
128 Attempts to parse the given filepath.
130 Args:
131 filepath (str or Path): filepath to asset file.
133 Raises:
134 ValidationError: If parse fails.
135 ValidationError: If asset directory name is invalid.
136 '''
137 filepath = Path(filepath)
138 try:
139 data = AssetNameParser(self.filename_fields).parse(filepath.name)
140 except ParseException as e:
141 raise ValidationError(repr(e))
143 if self.asset_type == 'file':
144 return
146 parser = AssetNameParser(self.asset_name_fields)
147 actual = self.get_asset_path(filepath).name
148 try:
149 parser.parse(actual)
150 except ParseException as e:
151 raise ValidationError(repr(e))
153 expected = parser.to_string(data)
154 if actual != expected:
155 msg = 'Invalid asset directory name. '
156 msg += f'Expecting: {expected}. Found: {actual} in '
157 msg += f'{filepath.as_posix()}.'
158 raise ValidationError(msg)
160 def get_filename_traits(self, filepath):
161 # type: (Union[str, Path]) -> Dict[str, Any]
162 '''
163 Returns a dictionary of filename traits from given filepath.
164 Returns error in filename_error key if one is encountered.
166 Args:
167 filepath (str or Path): filepath to asset file.
169 Returns:
170 dict: Traits.
171 '''
172 try:
173 return AssetNameParser(self.filename_fields)\
174 .parse(Path(filepath).name)
175 except ParseException as e:
176 return dict(filename_error=hbt.error_to_string(e))
178 def get_file_traits(self, filepath):
179 # type: (Union[str, Path]) -> Dict
180 '''
181 Returns a dictionary of file traits from given filepath.
182 Returns error in respective key if one is encountered.
184 Args:
185 filepath (str or Path): filepath to asset file.
187 Returns:
188 dict: Traits.
189 '''
190 output = {}
191 for name, func in self.file_traits.items():
192 try:
193 output[name] = func(filepath)
194 except Exception as e:
195 output[name + '_error'] = hbt.error_to_string(e)
196 return output
198 def get_traits(self, filepath):
199 # type: (Union[str, Path]) -> Dict[str, Any]
200 '''
201 Returns a dictionary of file and filename traits from given filepath.
202 Errors are captured in their respective keys.
204 Args:
205 filepath (str or Path): filepath to asset file.
207 Returns:
208 dict: Traits.
209 '''
210 traits = self.get_filename_traits(filepath)
211 traits.update(self.get_file_traits(filepath))
212 return traits
214 def get_name_patterns(self):
215 # type: () -> Tuple[str, str]
216 '''
217 Generates asset name and filename patterns from class fields.
219 Returns:
220 tuple(str): Asset name pattern, filename pattern.
221 '''
222 def get_patterns(fields):
223 output = []
224 c_pad = AssetNameParser.COORDINATE_PADDING
225 f_pad = AssetNameParser.FRAME_PADDING
226 v_pad = AssetNameParser.VERSION_PADDING
227 sep = AssetNameParser.TOKEN_SEPARATOR
228 for field in fields:
229 item = _INDICATOR_LUT[field]
230 if field == 'version':
231 item += '{' + field + f':0{v_pad}d' + '}'
232 elif field == 'frame':
233 item += '{' + field + f':0{f_pad}d' + '}'
234 elif field == 'coordinate':
235 temp = []
236 for i, _ in enumerate(self.coordinate[0]):
237 temp.append('{' + f'{field}[{i}]:0{c_pad}d' + '}')
238 temp = sep.join(temp)
239 item += temp
240 else:
241 item += '{' + field + '}'
242 output.append(item)
243 return output
245 asset = get_patterns(self.asset_name_fields) # type: Any
246 asset = AssetNameParser.FIELD_SEPARATOR.join(asset)
248 patterns = get_patterns(self.filename_fields)
249 filename = ''
250 if self.filename_fields[-1] == 'extension':
251 filename = AssetNameParser.FIELD_SEPARATOR.join(patterns[:-1])
252 filename += patterns[-1]
253 else:
254 filename = AssetNameParser.FIELD_SEPARATOR.join(patterns)
255 return asset, filename
257 def _to_filepaths(self, root, pattern):
258 # type: (Union[str, Path], str) -> List[str]
259 '''
260 Generates a complete list of filepaths given a root directory and
261 filepath pattern.
263 Args:
264 root (str or Path): Directory containing asset.
265 pattern (str): Filepath pattern.
267 Returns:
268 list[str]: List of filepaths.
269 '''
270 filepaths = []
271 for i, _ in enumerate(self.extension):
272 fields = set(self.asset_name_fields).union(self.filename_fields) # type: Any
273 fields = {k: getattr(self, k)[i] for k in fields}
274 filepath = pattern.format(**fields)
275 filepath = Path(root, filepath).as_posix()
276 filepaths.append(filepath)
277 return filepaths
280class FileSpecificationBase(SpecificationBase):
281 '''
282 The base class for asset that consist of a single file.
284 Attributes:
285 asset_type (str): File.
286 '''
287 asset_type = 'file' # type: str
289 def get_asset_path(self, filepath):
290 # type: (Union[str, Path]) -> Path
291 '''
292 Returns the filepath.
294 Args:
295 filepath (str or Path): filepath to asset file.
297 Returns:
298 Path: Asset path.
299 '''
300 return Path(filepath)
302 def to_filepaths(self, root):
303 # type: (Union[str, Path]) -> List[str]
304 '''
305 Generates a complete list of filepaths given a root directory and
306 filepath pattern.
308 Args:
309 root (str or Path): Directory containing asset.
310 pattern (str): Filepath pattern.
312 Returns:
313 list[str]: List of filepaths.
314 '''
315 _, filename = self.get_name_patterns()
316 return self._to_filepaths(root, filename)
319class SequenceSpecificationBase(SpecificationBase):
320 '''
321 The base class for assets that consist of a sequence of files under a single
322 directory.
324 Attributes:
325 asset_type (str): Sequence.
326 '''
327 asset_type = 'sequence' # type: str
329 def get_asset_path(self, filepath):
330 # type: (Union[str, Path]) -> Path
331 '''
332 Returns the directory containing the asset files.
334 Args:
335 filepath (str or Path): filepath to asset file.
337 Returns:
338 Path: Asset path.
339 '''
340 return Path(filepath).parents[0]
342 def to_filepaths(self, root):
343 # type: (Union[str, Path]) -> List[str]
344 '''
345 Generates a complete list of filepaths given a root directory and
346 filepath pattern.
348 Args:
349 root (str or Path): Directory containing asset.
350 pattern (str): Filepath pattern.
352 Returns:
353 list[str]: List of filepaths.
354 '''
355 asset, filename = self.get_name_patterns()
356 pattern = Path(asset, filename).as_posix()
357 return self._to_filepaths(root, pattern)
360class ComplexSpecificationBase(SpecificationBase):
361 '''
362 The base class for assets that consist of multiple directories of files.
364 Attributes:
365 asset_type (str): Complex.
366 '''
367 asset_type = 'complex' # type: str