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

146 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-07-02 10:16 +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 __future__ import annotations 

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 | None: 

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( 

37 entity_type: str, entity_shape: str | None 

38) -> dict[str, dict]: 

39 """ 

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

41 

42 Returns: 

43 Dictionary mapping display names to property configs 

44 """ 

45 display_rules = get_display_rules() 

46 if not display_rules: 

47 return {} 

48 

49 matching_rule = find_matching_rule(entity_type, entity_shape, display_rules) 

50 if not matching_rule: 

51 return {} 

52 

53 virtual_property_configs = {} 

54 for prop_config in matching_rule.get("displayProperties", []): 

55 if prop_config.get("isVirtual"): 

56 display_name = prop_config.get("displayName") 

57 if display_name: 

58 virtual_property_configs[display_name] = prop_config 

59 

60 return virtual_property_configs 

61 

62 

63def get_virtual_properties_for_entity( 

64 highest_priority_class: str, entity_shape: str | None 

65) -> list[tuple[str, dict]]: 

66 """ 

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

68 

69 Args: 

70 highest_priority_class: The highest priority class for the entity 

71 entity_shape: The shape for the entity 

72 

73 Returns: 

74 List of tuples (displayName, property_config) 

75 """ 

76 virtual_property_configs = _get_virtual_property_configs( 

77 highest_priority_class, entity_shape 

78 ) 

79 

80 return [ 

81 (display_name, config) 

82 for display_name, config in virtual_property_configs.items() 

83 ] 

84 

85 

86def apply_field_overrides( 

87 form_field_data: dict, field_overrides: dict, current_entity_uri: str | None = None 

88) -> dict: 

89 """ 

90 Apply field overrides to form field data. 

91 

92 Args: 

93 form_field_data: Dictionary from form_fields with structure {property_uri: 

94 [details]} 

95 field_overrides: Dictionary with field override rules 

96 

97 Returns: 

98 Modified form field data with overrides applied 

99 """ 

100 modified_data = {} 

101 

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

103 if property_uri in field_overrides: 

104 override = field_overrides[property_uri] 

105 modified_details_list = [] 

106 

107 for details in details_list: 

108 modified_details = details.copy() 

109 

110 if "shouldBeDisplayed" in override: 

111 modified_details["shouldBeDisplayed"] = override[ 

112 "shouldBeDisplayed" 

113 ] 

114 

115 if "displayName" in override: 

116 modified_details["displayName"] = override["displayName"] 

117 

118 if "value" in override: 

119 value = override["value"] 

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

121 value = current_entity_uri 

122 modified_details["hasValue"] = value 

123 if "nestedShape" in modified_details: 

124 modified_details["nestedShape"] = [] 

125 

126 modified_details_list.append(modified_details) 

127 

128 visible_details = [ 

129 details 

130 for details in modified_details_list 

131 if details.get("shouldBeDisplayed", True) 

132 ] 

133 if visible_details: 

134 modified_data[property_uri] = visible_details 

135 else: 

136 modified_data[property_uri] = details_list 

137 

138 return modified_data 

139 

140 

141def transform_changes_with_virtual_properties(changes: list[dict]) -> list[dict]: 

142 """ 

143 Transform a list of changes, expanding virtual properties into actual entity 

144 creations/deletions. 

145 

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

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

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

149 

150 Args: 

151 changes: List of change dictionaries from the API 

152 

153 Returns: 

154 Expanded list of changes with virtual properties transformed 

155 """ 

156 processed_changes = [] 

157 

158 for change in changes: 

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

160 data = change["data"] 

161 modified_data, virtual_entities = process_virtual_properties_in_create_data( 

162 data, change.get("subject") 

163 ) 

164 

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

166 if modified_data.get("properties"): 

167 main_change = change.copy() 

168 main_change["data"] = modified_data 

169 processed_changes.append(main_change) 

170 

171 for virtual_entity in virtual_entities: 

172 virtual_change = { 

173 "action": "create", 

174 "subject": None, # Will be generated 

175 "data": virtual_entity, 

176 } 

177 processed_changes.append(virtual_change) 

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

179 transformed_change = transform_virtual_property_deletion(change) 

180 if transformed_change: 

181 processed_changes.append(transformed_change) 

182 else: 

183 processed_changes.append(change) 

184 else: 

185 processed_changes.append(change) 

186 

187 return processed_changes 

188 

189 

190def process_virtual_properties_in_create_data( 

191 data: dict, subject_uri: str | None = None 

192) -> tuple[dict, list[dict]]: 

193 """ 

194 Process virtual properties in entity creation data. 

195 

196 Virtual properties need to be transformed into actual entity creations with proper 

197 relationships. 

198 

199 Args: 

200 data: The entity creation data containing properties 

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

202 

203 Returns: 

204 Tuple of: 

205 - Modified data with virtual properties removed 

206 - List of intermediate entities to create 

207 """ 

208 entity_type = _validate_entity_data(data) 

209 if not entity_type: 

210 return data, [] 

211 

212 entity_shape = data.get("entity_shape") 

213 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

214 

215 if not virtual_property_configs: 

216 return data, [] 

217 

218 regular_properties = {} 

219 virtual_entities = [] 

220 

221 for property_name, property_values in data["properties"].items(): 

222 if property_name in virtual_property_configs: 

223 config = virtual_property_configs[property_name] 

224 entities = process_virtual_property_values( 

225 property_values, config, subject_uri 

226 ) 

227 virtual_entities.extend(entities) 

228 else: 

229 regular_properties[property_name] = property_values 

230 

231 modified_data = data.copy() 

232 modified_data["properties"] = regular_properties 

233 

234 return modified_data, virtual_entities 

235 

236 

237def transform_entity_creation_with_virtual_properties( 

238 structured_data: dict, created_entity_uri: str 

239) -> list[dict]: 

240 """ 

241 Transform virtual properties in entity creation data after the main entity has been 

242 created. 

243 

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

245 properties 

246 after the main entity URI is available. 

247 

248 Args: 

249 structured_data: The original entity creation data 

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

251 

252 Returns: 

253 List of intermediate entities to create 

254 """ 

255 entity_type = _validate_entity_data(structured_data) 

256 if not entity_type: 

257 return [] 

258 

259 entity_shape = structured_data.get("entity_shape") 

260 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

261 

262 if not virtual_property_configs: 

263 return [] 

264 

265 virtual_entities = [] 

266 

267 for property_name, property_values in structured_data["properties"].items(): 

268 if property_name in virtual_property_configs: 

269 config = virtual_property_configs[property_name] 

270 entities = process_virtual_property_values( 

271 property_values, config, created_entity_uri 

272 ) 

273 virtual_entities.extend(entities) 

274 

275 return virtual_entities 

276 

277 

278def remove_virtual_properties_from_creation_data(structured_data: dict) -> dict: 

279 """ 

280 Remove virtual properties from entity creation data, 

281 leaving only regular properties. 

282 

283 Args: 

284 structured_data: The original entity creation data 

285 

286 Returns: 

287 Modified structured data with virtual properties removed 

288 """ 

289 entity_type = _validate_entity_data(structured_data) 

290 if not entity_type: 

291 return structured_data 

292 

293 entity_shape = structured_data.get("entity_shape") 

294 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape) 

295 

296 if not virtual_property_configs: 

297 return structured_data 

298 

299 # Create modified structured data without virtual properties 

300 modified_data = structured_data.copy() 

301 modified_data["properties"] = { 

302 property_name: property_values 

303 for property_name, property_values in structured_data["properties"].items() 

304 if property_name not in virtual_property_configs 

305 } 

306 return modified_data 

307 

308 

309def transform_virtual_property_deletion(change: dict) -> dict | None: 

310 """ 

311 Transform a virtual property deletion into an entity deletion. 

312 

313 When deleting a virtual property value, we need to 

314 delete the entire intermediate entity 

315 that was created to implement that virtual property. 

316 

317 Args: 

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

319 Must have 'is_virtual' flag set to True and 'object' containing the 

320 entity URI. 

321 

322 Returns: 

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

324 property 

325 """ 

326 if not change.get("is_virtual", False): 

327 return None 

328 

329 object_value = change.get("object") 

330 if not object_value: 

331 return None 

332 

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

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

335 return {"action": "delete", "subject": object_value} 

336 

337 

338def process_virtual_property_values( 

339 values: list, config: dict, subject_uri: str | None = None 

340) -> list[dict]: 

341 """ 

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

343 

344 Args: 

345 values: List of values for the virtual property 

346 config: Virtual property configuration 

347 subject_uri: URI of the main entity 

348 

349 Returns: 

350 List of intermediate entities to create 

351 """ 

352 entities = [] 

353 implementation = config.get("implementedVia", {}) 

354 target = implementation.get("target", {}) 

355 field_overrides = implementation.get("fieldOverrides", {}) 

356 

357 if not target.get("class") and not target.get("shape"): 

358 return entities 

359 

360 for value in values: 

361 if isinstance(value, dict): 

362 entity = { 

363 "entity_type": target.get("class"), 

364 "entity_shape": target.get("shape"), 

365 "properties": {}, 

366 } 

367 

368 if "properties" in value: 

369 entity["properties"] = value["properties"].copy() 

370 

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

372 if not override.get("shouldBeDisplayed", True) and "value" in override: 

373 override_value = override["value"] 

374 if override_value == "${currentEntity}" and subject_uri: 

375 entity["properties"][field_uri] = [ 

376 {"is_existing_entity": True, "entity_uri": subject_uri} 

377 ] 

378 else: 

379 entity["properties"][field_uri] = [override_value] 

380 

381 if "entity_shape" in value: 

382 entity["entity_shape"] = value["entity_shape"] 

383 

384 entities.append(entity) 

385 

386 return entities