Coverage for heritrace / utils / virtual_properties.py: 100%

151 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-21 12:56 +0000

1# SPDX-FileCopyrightText: 2025 Arcangelo Massari <arcangelo.massari@unibo.it> 

2# 

3# SPDX-License-Identifier: ISC 

4 

5""" 

6Virtual properties utilities for display rules. 

7 

8This module contains functions for processing virtual properties which allow 

9entities to display computed or derived relationships that don't exist directly 

10in the knowledge graph. 

11""" 

12 

13from typing import Dict, List, Tuple, Any, Optional 

14 

15from heritrace.extensions import get_display_rules 

16from heritrace.utils.display_rules_utils import find_matching_rule 

17 

18 

19def _validate_entity_data(data: Dict[str, Any]) -> Optional[str]: 

20 """ 

21 Validate entity data for virtual property processing. 

22 

23 Returns: 

24 entity_type if valid, None if invalid 

25 """ 

26 if not data.get('properties'): 

27 return None 

28 

29 entity_type = data.get('entity_type') 

30 if not entity_type: 

31 return None 

32 

33 return entity_type 

34 

35 

36def _get_virtual_property_configs(entity_type: str, entity_shape: str) -> Dict[str, Dict]: 

37 """ 

38 Get virtual property configurations for an entity type and shape. 

39 

40 Returns: 

41 Dictionary mapping display names to property configs 

42 """ 

43 display_rules = get_display_rules() 

44 if not display_rules: 

45 return {} 

46 

47 matching_rule = find_matching_rule(entity_type, entity_shape, display_rules) 

48 if not matching_rule: 

49 return {} 

50 

51 virtual_property_configs = {} 

52 for prop_config in matching_rule.get('displayProperties', []): 

53 if prop_config.get('isVirtual'): 

54 display_name = prop_config.get('displayName') 

55 if display_name: 

56 virtual_property_configs[display_name] = prop_config 

57 

58 return virtual_property_configs 

59 

60 

61def get_virtual_properties_for_entity(highest_priority_class: str, entity_shape: str) -> List[Tuple[str, Dict]]: 

62 """ 

63 Extract virtual properties configured for a specific entity class and shape. 

64 

65 Args: 

66 highest_priority_class: The highest priority class for the entity 

67 entity_shape: The shape for the entity 

68 

69 Returns: 

70 List of tuples (displayName, property_config) 

71 """ 

72 virtual_property_configs = _get_virtual_property_configs(highest_priority_class, entity_shape) 

73 

74 return [(display_name, config) for display_name, config in virtual_property_configs.items()] 

75 

76 

77def apply_field_overrides(form_field_data: Dict, field_overrides: Dict, current_entity_uri: str = None) -> Dict: 

78 """ 

79 Apply field overrides to form field data. 

80 

81 Args: 

82 form_field_data: Dictionary from form_fields with structure {property_uri: [details]} 

83 field_overrides: Dictionary with field override rules 

84 

85 Returns: 

86 Modified form field data with overrides applied 

87 """ 

88 modified_data = {} 

89 

90 for property_uri, details_list in form_field_data.items(): 

91 if property_uri in field_overrides: 

92 override = field_overrides[property_uri] 

93 modified_details_list = [] 

94 

95 for details in details_list: 

96 modified_details = details.copy() 

97 

98 if "shouldBeDisplayed" in override: 

99 modified_details["shouldBeDisplayed"] = override["shouldBeDisplayed"] 

100 

101 if "displayName" in override: 

102 modified_details["displayName"] = override["displayName"] 

103 

104 if "value" in override: 

105 value = override["value"] 

106 if value == "${currentEntity}" and current_entity_uri: 

107 value = current_entity_uri 

108 modified_details["hasValue"] = value 

109 if "nestedShape" in modified_details: 

110 modified_details["nestedShape"] = [] 

111 

112 modified_details_list.append(modified_details) 

113 

114 visible_details = [details for details in modified_details_list if details.get('shouldBeDisplayed', True)] 

115 if visible_details: 

116 modified_data[property_uri] = visible_details 

117 else: 

118 modified_data[property_uri] = details_list 

119 

120 return modified_data 

121 

122def transform_changes_with_virtual_properties(changes: List[Dict[str, Any]]) -> List[Dict[str, Any]]: 

123 """ 

124 Transform a list of changes, expanding virtual properties into actual entity creations/deletions. 

125 

126 This is the main function to call from the API to handle virtual properties. 

127 It processes all changes and returns an expanded list where virtual properties 

128 have been converted to proper entity creation or deletion changes. 

129 

130 Args: 

131 changes: List of change dictionaries from the API 

132 

133 Returns: 

134 Expanded list of changes with virtual properties transformed 

135 """ 

136 processed_changes = [] 

137 

138 for change in changes: 

139 if change["action"] == "create" and change.get("data"): 

140 data = change["data"] 

141 modified_data, virtual_entities = process_virtual_properties_in_create_data( 

142 data, 

143 change.get("subject") 

144 ) 

145 

146 # Only add the main change if there are non-virtual properties remaining 

147 if modified_data.get('properties'): 

148 main_change = change.copy() 

149 main_change["data"] = modified_data 

150 processed_changes.append(main_change) 

151 

152 for virtual_entity in virtual_entities: 

153 virtual_change = { 

154 "action": "create", 

155 "subject": None, # Will be generated 

156 "data": virtual_entity 

157 } 

158 processed_changes.append(virtual_change) 

159 elif change["action"] == "delete" and change.get("is_virtual", False): 

160 transformed_change = transform_virtual_property_deletion(change) 

161 if transformed_change: 

162 processed_changes.append(transformed_change) 

163 else: 

164 processed_changes.append(change) 

165 else: 

166 processed_changes.append(change) 

167 

168 return processed_changes 

169 

170 

171def process_virtual_properties_in_create_data(data: Dict[str, Any], subject_uri: str = None) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: 

172 """ 

173 Process virtual properties in entity creation data. 

174 

175 Virtual properties need to be transformed into actual entity creations with proper relationships. 

176 

177 Args: 

178 data: The entity creation data containing properties 

179 subject_uri: The URI of the subject entity being created/edited 

180 

181 Returns: 

182 Tuple of: 

183 - Modified data with virtual properties removed 

184 - List of intermediate entities to create 

185 """ 

186 entity_type = _validate_entity_data(data) 

187 if not entity_type: 

188 return data, [] 

189 

190 entity_shape = data.get('entity_shape') 

191 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

192 

193 if not virtual_property_configs: 

194 return data, [] 

195 

196 regular_properties = {} 

197 virtual_entities = [] 

198 

199 for property_name, property_values in data['properties'].items(): 

200 if property_name in virtual_property_configs: 

201 config = virtual_property_configs[property_name] 

202 entities = process_virtual_property_values( 

203 property_values, 

204 config, 

205 subject_uri 

206 ) 

207 virtual_entities.extend(entities) 

208 else: 

209 regular_properties[property_name] = property_values 

210 

211 modified_data = data.copy() 

212 modified_data['properties'] = regular_properties 

213 

214 return modified_data, virtual_entities 

215 

216 

217def transform_entity_creation_with_virtual_properties(structured_data: Dict[str, Any], created_entity_uri: str) -> List[Dict[str, Any]]: 

218 """ 

219 Transform virtual properties in entity creation data after the main entity has been created. 

220 

221 This function is specifically for entity creation where we need to process virtual properties 

222 after the main entity URI is available. 

223 

224 Args: 

225 structured_data: The original entity creation data 

226 created_entity_uri: The URI of the entity that was just created 

227 

228 Returns: 

229 List of intermediate entities to create 

230 """ 

231 entity_type = _validate_entity_data(structured_data) 

232 if not entity_type: 

233 return [] 

234 

235 entity_shape = structured_data.get('entity_shape') 

236 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

237 

238 if not virtual_property_configs: 

239 return [] 

240 

241 virtual_entities = [] 

242 

243 for property_name, property_values in structured_data['properties'].items(): 

244 if property_name in virtual_property_configs: 

245 config = virtual_property_configs[property_name] 

246 entities = process_virtual_property_values( 

247 property_values, 

248 config, 

249 created_entity_uri 

250 ) 

251 virtual_entities.extend(entities) 

252 

253 return virtual_entities 

254 

255 

256def remove_virtual_properties_from_creation_data(structured_data: Dict[str, Any]) -> Dict[str, Any]: 

257 """ 

258 Remove virtual properties from entity creation data, leaving only regular properties. 

259 

260 Args: 

261 structured_data: The original entity creation data 

262 

263 Returns: 

264 Modified structured data with virtual properties removed 

265 """ 

266 entity_type = _validate_entity_data(structured_data) 

267 if not entity_type: 

268 return structured_data 

269 

270 entity_shape = structured_data.get('entity_shape') 

271 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

272 

273 if not virtual_property_configs: 

274 return structured_data 

275 

276 # Create modified structured data without virtual properties 

277 modified_data = structured_data.copy() 

278 modified_properties = {} 

279 

280 for property_name, property_values in structured_data['properties'].items(): 

281 if property_name not in virtual_property_configs: 

282 modified_properties[property_name] = property_values 

283 

284 modified_data['properties'] = modified_properties 

285 return modified_data 

286 

287 

288def transform_virtual_property_deletion(change: Dict[str, Any]) -> Optional[Dict[str, Any]]: 

289 """ 

290 Transform a virtual property deletion into an entity deletion. 

291 

292 When deleting a virtual property value, we need to delete the entire intermediate entity 

293 that was created to implement that virtual property. 

294 

295 Args: 

296 change: The original delete change containing the virtual property. 

297 Must have 'is_virtual' flag set to True and 'object' containing the entity URI. 

298 

299 Returns: 

300 A new delete change for the intermediate entity, or None if not a virtual property 

301 """ 

302 if not change.get('is_virtual', False): 

303 return None 

304 

305 object_value = change.get('object') 

306 if not object_value: 

307 return None 

308 

309 # The object value is the URI of the intermediate entity to delete 

310 # We delete the entire entity (no predicate specified) 

311 return { 

312 'action': 'delete', 

313 'subject': object_value 

314 } 

315 

316 

317def process_virtual_property_values(values: List[Any], config: Dict[str, Any], subject_uri: str = None) -> List[Dict[str, Any]]: 

318 """ 

319 Process values of a virtual property to create intermediate entities. 

320 

321 Args: 

322 values: List of values for the virtual property 

323 config: Virtual property configuration 

324 subject_uri: URI of the main entity 

325 

326 Returns: 

327 List of intermediate entities to create 

328 """ 

329 entities = [] 

330 implementation = config.get('implementedVia', {}) 

331 target = implementation.get('target', {}) 

332 field_overrides = implementation.get('fieldOverrides', {}) 

333 

334 if not target.get('class') and not target.get('shape'): 

335 return entities 

336 

337 for value in values: 

338 if isinstance(value, dict): 

339 entity = { 

340 'entity_type': target.get('class'), 

341 'entity_shape': target.get('shape'), 

342 'properties': {} 

343 } 

344 

345 if 'properties' in value: 

346 entity['properties'] = value['properties'].copy() 

347 

348 for field_uri, override in field_overrides.items(): 

349 if not override.get('shouldBeDisplayed', True): 

350 if 'value' in override: 

351 override_value = override['value'] 

352 if override_value == '${currentEntity}' and subject_uri: 

353 entity['properties'][field_uri] = [{ 

354 'is_existing_entity': True, 

355 'entity_uri': subject_uri 

356 }] 

357 else: 

358 entity['properties'][field_uri] = [override_value] 

359 

360 if 'entity_shape' in value: 

361 entity['entity_shape'] = value['entity_shape'] 

362 

363 entities.append(entity) 

364 

365 return entities