Coverage for backend/django/diagnostics/services.py: 14%

36 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-02-12 01:47 +0000

1""" 

2Diagnostics service functions. 

3 

4This module contains the business logic for diagnostics validation, extracted 

5from views.py to keep views thin and make the logic reusable/testable. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import TYPE_CHECKING, TypeAlias 

11 

12from diagnostics.rules.engine import build_rule_context, evaluate_rules 

13 

14if TYPE_CHECKING: 

15 from core.auxiliary.models.PropertyInfo import PropertyInfo 

16 from core.auxiliary.models.PropertyValue import PropertyValue 

17 from core.auxiliary.models.PropertySet import PropertySet 

18 from flowsheetInternals.unitops.models.SimulationObject import SimulationObject 

19 

20 

21# Type aliases for clarity 

22FindingDict: TypeAlias = dict[str, str | int | float | None] 

23ErrorDict: TypeAlias = dict[str, str] 

24ValidationResult: TypeAlias = tuple[list[FindingDict] | None, ErrorDict | None] 

25 

26 

27def validate_property_change( 

28 obj: "SimulationObject", 

29 property_id: int | None, 

30 property_key: str | None, 

31 property_value_id: int | None, 

32 new_value: float | str | None, 

33) -> ValidationResult: 

34 """ 

35 Validate a proposed property change against diagnostic rules. 

36  

37 This is a pure "what-if" validation that runs rules against a proposed value 

38 without making any database changes or in-memory mutations. Perfect for: 

39 - Frontend inline validation (user typing in a field) 

40 - LLM-suggested values (test suggestions before applying them) 

41 - Pre-solve validation (check if a change would violate constraints) 

42  

43 Args: 

44 obj: The SimulationObject with properties prefetched. 

45 property_id: ID of the property to validate (optional if property_key given). 

46 property_key: Key of the property to validate (optional if property_id given). 

47 property_value_id: ID of the specific value (required for indexed properties). 

48 new_value: The proposed new value to validate. 

49  

50 Returns: 

51 ValidationResult tuple of (findings, error): 

52 - On success: (list of FindingDict, None) 

53 - On error: (None, ErrorDict with "error" key) 

54  

55 Example: 

56 # Validate a user-entered value 

57 findings, error = validate_property_change(obj, property_id=123, new_value=1.5) 

58 if error: 

59 return Response(error, status=400) 

60 return Response({"findings": findings}) 

61  

62 # Validate an LLM suggestion 

63 llm_suggested_value = 0.85 

64 findings, error = validate_property_change(obj, property_key="efficiency",  

65 new_value=llm_suggested_value) 

66 if not error and not findings: 

67 # Safe to apply - no rule violations 

68 apply_llm_suggestion(llm_suggested_value) 

69 """ 

70 

71 # Property values live under: SimulationObject -> PropertySet -> PropertyInfo -> PropertyValue. 

72 # If the object has no PropertySet attached, there's nothing to validate. 

73 from core.auxiliary.models.PropertySet import PropertySet 

74 

75 try: 

76 prop_set: PropertySet = obj.properties 

77 except PropertySet.DoesNotExist: 

78 return None, {"error": "Property set not found for object"} 

79 

80 # Resolve the target property (serializer guarantees at least one of id/key is provided). 

81 if property_id is not None: 

82 target_prop: PropertyInfo | None = prop_set.ContainedProperties.filter(id=property_id).first() 

83 else: 

84 target_prop = prop_set.ContainedProperties.filter(key=property_key).first() 

85 

86 if not target_prop: 

87 return None, {"error": "Property not found for object"} 

88 

89 # Resolve the target value (needed for indexed properties). 

90 target_value: PropertyValue | None = None 

91 values: list[PropertyValue] = list(target_prop.values.all()) 

92 

93 if property_value_id is not None: 

94 # Explicit value ID provided 

95 target_value = next((v for v in values if v.id == property_value_id), None) 

96 if not target_value: 

97 return None, {"error": "Property value not found for property"} 

98 

99 elif len(values) == 1 and not values[0].indexedItems.exists(): 

100 # Single non-indexed value - auto-select it 

101 target_value = values[0] 

102 

103 # Check for ambiguity: new_value provided but we don't know which value to update 

104 if new_value is not None and target_value is None: 

105 return None, {"error": "Ambiguous property value; provide property_value_id for indexed properties"} 

106 

107 # Validate the proposed value without any DB writes or in-memory mutations. 

108 # build_rule_context takes numeric_value directly as a parameter. 

109 if new_value is None: 

110 return [], None 

111 

112 try: 

113 numeric_value = float(new_value) 

114 except (TypeError, ValueError): 

115 return None, {"error": "new_value must be a number"} 

116 

117 # Run the diagnostic rules with the proposed value 

118 context = build_rule_context(obj, target_prop, numeric_value) 

119 findings: list[FindingDict] = [f.to_dict() for f in evaluate_rules(context)] 

120 return findings, None