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

151 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-10-13 17:12 +0000

1""" 

2Virtual properties utilities for display rules. 

3 

4This module contains functions for processing virtual properties which allow 

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

6in the knowledge graph. 

7""" 

8 

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

10 

11from heritrace.extensions import get_display_rules 

12from heritrace.utils.display_rules_utils import find_matching_rule 

13 

14 

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

16 """ 

17 Validate entity data for virtual property processing. 

18 

19 Returns: 

20 entity_type if valid, None if invalid 

21 """ 

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

23 return None 

24 

25 entity_type = data.get('entity_type') 

26 if not entity_type: 

27 return None 

28 

29 return entity_type 

30 

31 

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

33 """ 

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

35 

36 Returns: 

37 Dictionary mapping display names to property configs 

38 """ 

39 display_rules = get_display_rules() 

40 if not display_rules: 

41 return {} 

42 

43 matching_rule = find_matching_rule(entity_type, entity_shape, display_rules) 

44 if not matching_rule: 

45 return {} 

46 

47 virtual_property_configs = {} 

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

49 if prop_config.get('isVirtual'): 

50 display_name = prop_config.get('displayName') 

51 if display_name: 

52 virtual_property_configs[display_name] = prop_config 

53 

54 return virtual_property_configs 

55 

56 

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

58 """ 

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

60 

61 Args: 

62 highest_priority_class: The highest priority class for the entity 

63 entity_shape: The shape for the entity 

64 

65 Returns: 

66 List of tuples (displayName, property_config) 

67 """ 

68 virtual_property_configs = _get_virtual_property_configs(highest_priority_class, entity_shape) 

69 

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

71 

72 

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

74 """ 

75 Apply field overrides to form field data. 

76 

77 Args: 

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

79 field_overrides: Dictionary with field override rules 

80 

81 Returns: 

82 Modified form field data with overrides applied 

83 """ 

84 modified_data = {} 

85 

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

87 if property_uri in field_overrides: 

88 override = field_overrides[property_uri] 

89 modified_details_list = [] 

90 

91 for details in details_list: 

92 modified_details = details.copy() 

93 

94 if "shouldBeDisplayed" in override: 

95 modified_details["shouldBeDisplayed"] = override["shouldBeDisplayed"] 

96 

97 if "displayName" in override: 

98 modified_details["displayName"] = override["displayName"] 

99 

100 if "value" in override: 

101 value = override["value"] 

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

103 value = current_entity_uri 

104 modified_details["hasValue"] = value 

105 if "nestedShape" in modified_details: 

106 modified_details["nestedShape"] = [] 

107 

108 modified_details_list.append(modified_details) 

109 

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

111 if visible_details: 

112 modified_data[property_uri] = visible_details 

113 else: 

114 modified_data[property_uri] = details_list 

115 

116 return modified_data 

117 

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

119 """ 

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

121 

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

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

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

125 

126 Args: 

127 changes: List of change dictionaries from the API 

128 

129 Returns: 

130 Expanded list of changes with virtual properties transformed 

131 """ 

132 processed_changes = [] 

133 

134 for change in changes: 

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

136 data = change["data"] 

137 modified_data, virtual_entities = process_virtual_properties_in_create_data( 

138 data, 

139 change.get("subject") 

140 ) 

141 

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

143 if modified_data.get('properties'): 

144 main_change = change.copy() 

145 main_change["data"] = modified_data 

146 processed_changes.append(main_change) 

147 

148 for virtual_entity in virtual_entities: 

149 virtual_change = { 

150 "action": "create", 

151 "subject": None, # Will be generated 

152 "data": virtual_entity 

153 } 

154 processed_changes.append(virtual_change) 

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

156 transformed_change = transform_virtual_property_deletion(change) 

157 if transformed_change: 

158 processed_changes.append(transformed_change) 

159 else: 

160 processed_changes.append(change) 

161 else: 

162 processed_changes.append(change) 

163 

164 return processed_changes 

165 

166 

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

168 """ 

169 Process virtual properties in entity creation data. 

170 

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

172 

173 Args: 

174 data: The entity creation data containing properties 

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

176 

177 Returns: 

178 Tuple of: 

179 - Modified data with virtual properties removed 

180 - List of intermediate entities to create 

181 """ 

182 entity_type = _validate_entity_data(data) 

183 if not entity_type: 

184 return data, [] 

185 

186 entity_shape = data.get('entity_shape') 

187 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

188 

189 if not virtual_property_configs: 

190 return data, [] 

191 

192 regular_properties = {} 

193 virtual_entities = [] 

194 

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

196 if property_name in virtual_property_configs: 

197 config = virtual_property_configs[property_name] 

198 entities = process_virtual_property_values( 

199 property_values, 

200 config, 

201 subject_uri 

202 ) 

203 virtual_entities.extend(entities) 

204 else: 

205 regular_properties[property_name] = property_values 

206 

207 modified_data = data.copy() 

208 modified_data['properties'] = regular_properties 

209 

210 return modified_data, virtual_entities 

211 

212 

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

214 """ 

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

216 

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

218 after the main entity URI is available. 

219 

220 Args: 

221 structured_data: The original entity creation data 

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

223 

224 Returns: 

225 List of intermediate entities to create 

226 """ 

227 entity_type = _validate_entity_data(structured_data) 

228 if not entity_type: 

229 return [] 

230 

231 entity_shape = structured_data.get('entity_shape') 

232 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

233 

234 if not virtual_property_configs: 

235 return [] 

236 

237 virtual_entities = [] 

238 

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

240 if property_name in virtual_property_configs: 

241 config = virtual_property_configs[property_name] 

242 entities = process_virtual_property_values( 

243 property_values, 

244 config, 

245 created_entity_uri 

246 ) 

247 virtual_entities.extend(entities) 

248 

249 return virtual_entities 

250 

251 

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

253 """ 

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

255 

256 Args: 

257 structured_data: The original entity creation data 

258 

259 Returns: 

260 Modified structured data with virtual properties removed 

261 """ 

262 entity_type = _validate_entity_data(structured_data) 

263 if not entity_type: 

264 return structured_data 

265 

266 entity_shape = structured_data.get('entity_shape') 

267 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

268 

269 if not virtual_property_configs: 

270 return structured_data 

271 

272 # Create modified structured data without virtual properties 

273 modified_data = structured_data.copy() 

274 modified_properties = {} 

275 

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

277 if property_name not in virtual_property_configs: 

278 modified_properties[property_name] = property_values 

279 

280 modified_data['properties'] = modified_properties 

281 return modified_data 

282 

283 

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

285 """ 

286 Transform a virtual property deletion into an entity deletion. 

287 

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

289 that was created to implement that virtual property. 

290 

291 Args: 

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

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

294 

295 Returns: 

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

297 """ 

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

299 return None 

300 

301 object_value = change.get('object') 

302 if not object_value: 

303 return None 

304 

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

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

307 return { 

308 'action': 'delete', 

309 'subject': object_value 

310 } 

311 

312 

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

314 """ 

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

316 

317 Args: 

318 values: List of values for the virtual property 

319 config: Virtual property configuration 

320 subject_uri: URI of the main entity 

321 

322 Returns: 

323 List of intermediate entities to create 

324 """ 

325 entities = [] 

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

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

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

329 

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

331 return entities 

332 

333 for value in values: 

334 if isinstance(value, dict): 

335 entity = { 

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

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

338 'properties': {} 

339 } 

340 

341 if 'properties' in value: 

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

343 

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

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

346 if 'value' in override: 

347 override_value = override['value'] 

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

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

350 'is_existing_entity': True, 

351 'entity_uri': subject_uri 

352 }] 

353 else: 

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

355 

356 if 'entity_shape' in value: 

357 entity['entity_shape'] = value['entity_shape'] 

358 

359 entities.append(entity) 

360 

361 return entities