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
« 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
5"""
6Virtual properties utilities for display rules.
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"""
13from typing import Dict, List, Tuple, Any, Optional
15from heritrace.extensions import get_display_rules
16from heritrace.utils.display_rules_utils import find_matching_rule
19def _validate_entity_data(data: Dict[str, Any]) -> Optional[str]:
20 """
21 Validate entity data for virtual property processing.
23 Returns:
24 entity_type if valid, None if invalid
25 """
26 if not data.get('properties'):
27 return None
29 entity_type = data.get('entity_type')
30 if not entity_type:
31 return None
33 return entity_type
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.
40 Returns:
41 Dictionary mapping display names to property configs
42 """
43 display_rules = get_display_rules()
44 if not display_rules:
45 return {}
47 matching_rule = find_matching_rule(entity_type, entity_shape, display_rules)
48 if not matching_rule:
49 return {}
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
58 return virtual_property_configs
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.
65 Args:
66 highest_priority_class: The highest priority class for the entity
67 entity_shape: The shape for the entity
69 Returns:
70 List of tuples (displayName, property_config)
71 """
72 virtual_property_configs = _get_virtual_property_configs(highest_priority_class, entity_shape)
74 return [(display_name, config) for display_name, config in virtual_property_configs.items()]
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.
81 Args:
82 form_field_data: Dictionary from form_fields with structure {property_uri: [details]}
83 field_overrides: Dictionary with field override rules
85 Returns:
86 Modified form field data with overrides applied
87 """
88 modified_data = {}
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 = []
95 for details in details_list:
96 modified_details = details.copy()
98 if "shouldBeDisplayed" in override:
99 modified_details["shouldBeDisplayed"] = override["shouldBeDisplayed"]
101 if "displayName" in override:
102 modified_details["displayName"] = override["displayName"]
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"] = []
112 modified_details_list.append(modified_details)
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
120 return modified_data
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.
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.
130 Args:
131 changes: List of change dictionaries from the API
133 Returns:
134 Expanded list of changes with virtual properties transformed
135 """
136 processed_changes = []
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 )
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)
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)
168 return processed_changes
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.
175 Virtual properties need to be transformed into actual entity creations with proper relationships.
177 Args:
178 data: The entity creation data containing properties
179 subject_uri: The URI of the subject entity being created/edited
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, []
190 entity_shape = data.get('entity_shape')
191 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
193 if not virtual_property_configs:
194 return data, []
196 regular_properties = {}
197 virtual_entities = []
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
211 modified_data = data.copy()
212 modified_data['properties'] = regular_properties
214 return modified_data, virtual_entities
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.
221 This function is specifically for entity creation where we need to process virtual properties
222 after the main entity URI is available.
224 Args:
225 structured_data: The original entity creation data
226 created_entity_uri: The URI of the entity that was just created
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 []
235 entity_shape = structured_data.get('entity_shape')
236 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
238 if not virtual_property_configs:
239 return []
241 virtual_entities = []
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)
253 return virtual_entities
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.
260 Args:
261 structured_data: The original entity creation data
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
270 entity_shape = structured_data.get('entity_shape')
271 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
273 if not virtual_property_configs:
274 return structured_data
276 # Create modified structured data without virtual properties
277 modified_data = structured_data.copy()
278 modified_properties = {}
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
284 modified_data['properties'] = modified_properties
285 return modified_data
288def transform_virtual_property_deletion(change: Dict[str, Any]) -> Optional[Dict[str, Any]]:
289 """
290 Transform a virtual property deletion into an entity deletion.
292 When deleting a virtual property value, we need to delete the entire intermediate entity
293 that was created to implement that virtual property.
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.
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
305 object_value = change.get('object')
306 if not object_value:
307 return None
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 }
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.
321 Args:
322 values: List of values for the virtual property
323 config: Virtual property configuration
324 subject_uri: URI of the main entity
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', {})
334 if not target.get('class') and not target.get('shape'):
335 return entities
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 }
345 if 'properties' in value:
346 entity['properties'] = value['properties'].copy()
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]
360 if 'entity_shape' in value:
361 entity['entity_shape'] = value['entity_shape']
363 entities.append(entity)
365 return entities