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
« prev ^ index » next coverage.py v7.6.12, created at 2025-10-13 17:12 +0000
1"""
2Virtual properties utilities for display rules.
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"""
9from typing import Dict, List, Tuple, Any, Optional
11from heritrace.extensions import get_display_rules
12from heritrace.utils.display_rules_utils import find_matching_rule
15def _validate_entity_data(data: Dict[str, Any]) -> Optional[str]:
16 """
17 Validate entity data for virtual property processing.
19 Returns:
20 entity_type if valid, None if invalid
21 """
22 if not data.get('properties'):
23 return None
25 entity_type = data.get('entity_type')
26 if not entity_type:
27 return None
29 return entity_type
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.
36 Returns:
37 Dictionary mapping display names to property configs
38 """
39 display_rules = get_display_rules()
40 if not display_rules:
41 return {}
43 matching_rule = find_matching_rule(entity_type, entity_shape, display_rules)
44 if not matching_rule:
45 return {}
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
54 return virtual_property_configs
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.
61 Args:
62 highest_priority_class: The highest priority class for the entity
63 entity_shape: The shape for the entity
65 Returns:
66 List of tuples (displayName, property_config)
67 """
68 virtual_property_configs = _get_virtual_property_configs(highest_priority_class, entity_shape)
70 return [(display_name, config) for display_name, config in virtual_property_configs.items()]
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.
77 Args:
78 form_field_data: Dictionary from form_fields with structure {property_uri: [details]}
79 field_overrides: Dictionary with field override rules
81 Returns:
82 Modified form field data with overrides applied
83 """
84 modified_data = {}
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 = []
91 for details in details_list:
92 modified_details = details.copy()
94 if "shouldBeDisplayed" in override:
95 modified_details["shouldBeDisplayed"] = override["shouldBeDisplayed"]
97 if "displayName" in override:
98 modified_details["displayName"] = override["displayName"]
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"] = []
108 modified_details_list.append(modified_details)
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
116 return modified_data
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.
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.
126 Args:
127 changes: List of change dictionaries from the API
129 Returns:
130 Expanded list of changes with virtual properties transformed
131 """
132 processed_changes = []
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 )
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)
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)
164 return processed_changes
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.
171 Virtual properties need to be transformed into actual entity creations with proper relationships.
173 Args:
174 data: The entity creation data containing properties
175 subject_uri: The URI of the subject entity being created/edited
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, []
186 entity_shape = data.get('entity_shape')
187 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
189 if not virtual_property_configs:
190 return data, []
192 regular_properties = {}
193 virtual_entities = []
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
207 modified_data = data.copy()
208 modified_data['properties'] = regular_properties
210 return modified_data, virtual_entities
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.
217 This function is specifically for entity creation where we need to process virtual properties
218 after the main entity URI is available.
220 Args:
221 structured_data: The original entity creation data
222 created_entity_uri: The URI of the entity that was just created
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 []
231 entity_shape = structured_data.get('entity_shape')
232 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
234 if not virtual_property_configs:
235 return []
237 virtual_entities = []
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)
249 return virtual_entities
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.
256 Args:
257 structured_data: The original entity creation data
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
266 entity_shape = structured_data.get('entity_shape')
267 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
269 if not virtual_property_configs:
270 return structured_data
272 # Create modified structured data without virtual properties
273 modified_data = structured_data.copy()
274 modified_properties = {}
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
280 modified_data['properties'] = modified_properties
281 return modified_data
284def transform_virtual_property_deletion(change: Dict[str, Any]) -> Optional[Dict[str, Any]]:
285 """
286 Transform a virtual property deletion into an entity deletion.
288 When deleting a virtual property value, we need to delete the entire intermediate entity
289 that was created to implement that virtual property.
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.
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
301 object_value = change.get('object')
302 if not object_value:
303 return None
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 }
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.
317 Args:
318 values: List of values for the virtual property
319 config: Virtual property configuration
320 subject_uri: URI of the main entity
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', {})
330 if not target.get('class') and not target.get('shape'):
331 return entities
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 }
341 if 'properties' in value:
342 entity['properties'] = value['properties'].copy()
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]
356 if 'entity_shape' in value:
357 entity['entity_shape'] = value['entity_shape']
359 entities.append(entity)
361 return entities