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

1from typing import Any, Dict, List, Optional, Tuple, Union # noqa F401 

2 

3from pathlib import Path 

4import uuid 

5 

6from pyparsing import ParseException 

7from schematics import Model 

8from schematics.exceptions import ValidationError 

9from schematics.types import IntType, ListType, StringType 

10 

11from hidebound.core.parser import AssetNameParser 

12import hidebound.core.tools as hbt 

13import hidebound.core.validators as vd 

14# ------------------------------------------------------------------------------ 

15 

16 

17''' 

18Contains the abstract base classes for all Hidebound specifications. 

19''' 

20 

21 

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) 

31 

32 

33class SpecificationBase(Model): 

34 ''' 

35 The base class for all Hidebound specifications. 

36 

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] 

65 

66 def __init__(self, data={}): 

67 # type: (Optional[Dict[str, Any]]) -> None 

68 ''' 

69 Returns a new specification instance. 

70 

71 Args: 

72 data (dict, optional): Dictionary of asset data. 

73 ''' 

74 super().__init__(raw_data=data) 

75 

76 def get_asset_name(self, filepath): 

77 # type: (Union[str, Path]) -> str 

78 ''' 

79 Returns the expected asset name give a filepath. 

80 

81 Args: 

82 filepath (str or Path): filepath to asset file. 

83 

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 

91 

92 def get_asset_path(self, filepath): 

93 # type: (Union[str, Path]) -> Path 

94 ''' 

95 Returns the expected asset path given a filepath. 

96 

97 Args: 

98 filepath (str or Path): filepath to asset file. 

99 

100 Raises: 

101 NotImplementedError: If method not defined in subclass. 

102 

103 Returns: 

104 Path: Asset path. 

105 ''' 

106 msg = 'Method must be implemented in subclasses of SpecificationBase.' 

107 raise NotImplementedError(msg) 

108 

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. 

114 

115 Args: 

116 filepath (str or Path): filepath to asset file. 

117 

118 Returns: 

119 str: Asset id. 

120 ''' 

121 return str(uuid.uuid3( 

122 uuid.NAMESPACE_URL, self.get_asset_path(filepath).as_posix() 

123 )) 

124 

125 def validate_filepath(self, filepath): 

126 # type: (Union[str, Path]) -> None 

127 ''' 

128 Attempts to parse the given filepath. 

129 

130 Args: 

131 filepath (str or Path): filepath to asset file. 

132 

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

142 

143 if self.asset_type == 'file': 

144 return 

145 

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

152 

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) 

159 

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. 

165 

166 Args: 

167 filepath (str or Path): filepath to asset file. 

168 

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

177 

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. 

183 

184 Args: 

185 filepath (str or Path): filepath to asset file. 

186 

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 

197 

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. 

203 

204 Args: 

205 filepath (str or Path): filepath to asset file. 

206 

207 Returns: 

208 dict: Traits. 

209 ''' 

210 traits = self.get_filename_traits(filepath) 

211 traits.update(self.get_file_traits(filepath)) 

212 return traits 

213 

214 def get_name_patterns(self): 

215 # type: () -> Tuple[str, str] 

216 ''' 

217 Generates asset name and filename patterns from class fields. 

218 

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 

244 

245 asset = get_patterns(self.asset_name_fields) # type: Any 

246 asset = AssetNameParser.FIELD_SEPARATOR.join(asset) 

247 

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 

256 

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. 

262 

263 Args: 

264 root (str or Path): Directory containing asset. 

265 pattern (str): Filepath pattern. 

266 

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 

278 

279 

280class FileSpecificationBase(SpecificationBase): 

281 ''' 

282 The base class for asset that consist of a single file. 

283 

284 Attributes: 

285 asset_type (str): File. 

286 ''' 

287 asset_type = 'file' # type: str 

288 

289 def get_asset_path(self, filepath): 

290 # type: (Union[str, Path]) -> Path 

291 ''' 

292 Returns the filepath. 

293 

294 Args: 

295 filepath (str or Path): filepath to asset file. 

296 

297 Returns: 

298 Path: Asset path. 

299 ''' 

300 return Path(filepath) 

301 

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. 

307 

308 Args: 

309 root (str or Path): Directory containing asset. 

310 pattern (str): Filepath pattern. 

311 

312 Returns: 

313 list[str]: List of filepaths. 

314 ''' 

315 _, filename = self.get_name_patterns() 

316 return self._to_filepaths(root, filename) 

317 

318 

319class SequenceSpecificationBase(SpecificationBase): 

320 ''' 

321 The base class for assets that consist of a sequence of files under a single 

322 directory. 

323 

324 Attributes: 

325 asset_type (str): Sequence. 

326 ''' 

327 asset_type = 'sequence' # type: str 

328 

329 def get_asset_path(self, filepath): 

330 # type: (Union[str, Path]) -> Path 

331 ''' 

332 Returns the directory containing the asset files. 

333 

334 Args: 

335 filepath (str or Path): filepath to asset file. 

336 

337 Returns: 

338 Path: Asset path. 

339 ''' 

340 return Path(filepath).parents[0] 

341 

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. 

347 

348 Args: 

349 root (str or Path): Directory containing asset. 

350 pattern (str): Filepath pattern. 

351 

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) 

358 

359 

360class ComplexSpecificationBase(SpecificationBase): 

361 ''' 

362 The base class for assets that consist of multiple directories of files. 

363 

364 Attributes: 

365 asset_type (str): Complex. 

366 ''' 

367 asset_type = 'complex' # type: str