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
« 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
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 __future__ import annotations
15from heritrace.extensions import get_display_rules
16from heritrace.utils.display_rules_utils import find_matching_rule
19def _validate_entity_data(data: dict) -> str | None:
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(
37 entity_type: str, entity_shape: str | None
38) -> dict[str, dict]:
39 """
40 Get virtual property configurations for an entity type and shape.
42 Returns:
43 Dictionary mapping display names to property configs
44 """
45 display_rules = get_display_rules()
46 if not display_rules:
47 return {}
49 matching_rule = find_matching_rule(entity_type, entity_shape, display_rules)
50 if not matching_rule:
51 return {}
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
60 return virtual_property_configs
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.
69 Args:
70 highest_priority_class: The highest priority class for the entity
71 entity_shape: The shape for the entity
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 )
80 return [
81 (display_name, config)
82 for display_name, config in virtual_property_configs.items()
83 ]
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.
92 Args:
93 form_field_data: Dictionary from form_fields with structure {property_uri:
94 [details]}
95 field_overrides: Dictionary with field override rules
97 Returns:
98 Modified form field data with overrides applied
99 """
100 modified_data = {}
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 = []
107 for details in details_list:
108 modified_details = details.copy()
110 if "shouldBeDisplayed" in override:
111 modified_details["shouldBeDisplayed"] = override[
112 "shouldBeDisplayed"
113 ]
115 if "displayName" in override:
116 modified_details["displayName"] = override["displayName"]
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"] = []
126 modified_details_list.append(modified_details)
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
138 return modified_data
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.
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.
150 Args:
151 changes: List of change dictionaries from the API
153 Returns:
154 Expanded list of changes with virtual properties transformed
155 """
156 processed_changes = []
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 )
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)
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)
187 return processed_changes
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.
196 Virtual properties need to be transformed into actual entity creations with proper
197 relationships.
199 Args:
200 data: The entity creation data containing properties
201 subject_uri: The URI of the subject entity being created/edited
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, []
212 entity_shape = data.get("entity_shape")
213 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
215 if not virtual_property_configs:
216 return data, []
218 regular_properties = {}
219 virtual_entities = []
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
231 modified_data = data.copy()
232 modified_data["properties"] = regular_properties
234 return modified_data, virtual_entities
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.
244 This function is specifically for entity creation where we need to process virtual
245 properties
246 after the main entity URI is available.
248 Args:
249 structured_data: The original entity creation data
250 created_entity_uri: The URI of the entity that was just created
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 []
259 entity_shape = structured_data.get("entity_shape")
260 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
262 if not virtual_property_configs:
263 return []
265 virtual_entities = []
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)
275 return virtual_entities
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.
283 Args:
284 structured_data: The original entity creation data
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
293 entity_shape = structured_data.get("entity_shape")
294 virtual_property_configs = _get_virtual_property_configs(entity_type, entity_shape)
296 if not virtual_property_configs:
297 return structured_data
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
309def transform_virtual_property_deletion(change: dict) -> dict | None:
310 """
311 Transform a virtual property deletion into an entity deletion.
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.
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.
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
329 object_value = change.get("object")
330 if not object_value:
331 return None
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}
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.
344 Args:
345 values: List of values for the virtual property
346 config: Virtual property configuration
347 subject_uri: URI of the main entity
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", {})
357 if not target.get("class") and not target.get("shape"):
358 return entities
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 }
368 if "properties" in value:
369 entity["properties"] = value["properties"].copy()
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]
381 if "entity_shape" in value:
382 entity["entity_shape"] = value["entity_shape"]
384 entities.append(entity)
386 return entities