Skip to content

Commit 92688aa

Browse files
saponifi3dcathteng
authored andcommitted
feat(workflow_engine): Refactor the Data Condition Validator (#88050)
## Description - Separate the DataCondition and DataConditionGroup validators - Add tests to the BaseDataConditionGroupValidator - Add tests for BaseDataConditionValidator - Create `AbstractDataConditionValidator` to easily allow definitions, this meant we could completely remove the numeric validator as we can just use the abstract validator. --------- Co-authored-by: Cathy Teng <[email protected]>
1 parent 45c26ba commit 92688aa

File tree

16 files changed

+382
-163
lines changed

16 files changed

+382
-163
lines changed

src/sentry/incidents/endpoints/validators/__init__.py

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/sentry/incidents/endpoints/validators/numeric_comparison_validator.py

Lines changed: 0 additions & 48 deletions
This file was deleted.

src/sentry/incidents/metric_alert_detector.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from rest_framework import serializers
55

66
from sentry import audit_log
7-
from sentry.incidents.endpoints.validators import NumericComparisonConditionValidator
87
from sentry.snuba.models import QuerySubscription, SnubaQuery, SnubaQueryEventType
98
from sentry.snuba.snuba_query_validator import SnubaQueryValidator
109
from sentry.snuba.subscriptions import update_snuba_query
@@ -13,6 +12,9 @@
1312
BaseDataConditionGroupValidator,
1413
BaseDetectorTypeValidator,
1514
)
15+
from sentry.workflow_engine.endpoints.validators.base.data_condition import (
16+
AbstractDataConditionValidator,
17+
)
1618
from sentry.workflow_engine.models import DataConditionGroup, DataSource, Detector
1719
from sentry.workflow_engine.models.data_condition import Condition, DataCondition
1820
from sentry.workflow_engine.types import (
@@ -22,17 +24,53 @@
2224
)
2325

2426

25-
class MetricAlertComparisonConditionValidator(NumericComparisonConditionValidator):
27+
class MetricAlertComparisonConditionValidator(
28+
AbstractDataConditionValidator[float, DetectorPriorityLevel]
29+
):
2630
supported_conditions = frozenset((Condition.GREATER, Condition.LESS))
2731
supported_condition_results = frozenset(
2832
(DetectorPriorityLevel.HIGH, DetectorPriorityLevel.MEDIUM)
2933
)
30-
condition_group_id = serializers.IntegerField(required=True)
31-
id = serializers.IntegerField(required=False)
34+
35+
def validate_type(self, value: str) -> Condition:
36+
try:
37+
type = Condition(value)
38+
except ValueError:
39+
type = None
40+
41+
if type not in self.supported_conditions:
42+
raise serializers.ValidationError(f"Unsupported type {value}")
43+
44+
return type
45+
46+
def validate_comparison(self, value: float | int | str) -> float:
47+
try:
48+
value = float(value)
49+
except ValueError:
50+
raise serializers.ValidationError("A valid number is required.")
51+
52+
return value
53+
54+
def validate_condition_result(self, value: str) -> DetectorPriorityLevel:
55+
try:
56+
result = DetectorPriorityLevel(int(value))
57+
except ValueError:
58+
result = None
59+
60+
if result not in self.supported_condition_results:
61+
raise serializers.ValidationError("Unsupported condition result")
62+
63+
return result
3264

3365

3466
class MetricAlertConditionGroupValidator(BaseDataConditionGroupValidator):
35-
conditions = MetricAlertComparisonConditionValidator(many=True)
67+
conditions = serializers.ListField(required=True)
68+
69+
def validate_conditions(self, value):
70+
MetricAlertComparisonConditionValidator(data=value, many=True).is_valid(
71+
raise_exception=True
72+
)
73+
return value
3674

3775

3876
class MetricAlertsDetectorValidator(BaseDetectorTypeValidator):

src/sentry/workflow_engine/endpoints/validators/base/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
__all__ = [
2+
"AbstractDataConditionValidator",
23
"BaseActionValidator",
34
"BaseDataConditionGroupValidator",
45
"BaseDataConditionValidator",
@@ -8,6 +9,7 @@
89
]
910

1011
from .action import BaseActionValidator
11-
from .data_condition import BaseDataConditionGroupValidator, BaseDataConditionValidator
12+
from .data_condition import AbstractDataConditionValidator, BaseDataConditionValidator
13+
from .data_condition_group import BaseDataConditionGroupValidator
1214
from .data_source import BaseDataSourceValidator, DataSourceCreator
1315
from .detector import BaseDetectorTypeValidator

src/sentry/workflow_engine/endpoints/validators/base/action.py

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
from typing import Any, Generic, TypeVar
22

3-
from django.forms import ValidationError
4-
from jsonschema import ValidationError as JsonValidationError
5-
from jsonschema import validate
63
from rest_framework import serializers
74

85
from sentry.api.serializers.rest_framework import CamelSnakeModelSerializer
96
from sentry.db.models import Model
7+
from sentry.workflow_engine.endpoints.validators.utils import validate_json_schema
108
from sentry.workflow_engine.models import Action
119
from sentry.workflow_engine.registry import action_handler_registry
1210
from sentry.workflow_engine.types import ActionHandler
@@ -16,39 +14,19 @@
1614
ActionConfig = dict[str, Any]
1715

1816

19-
def validate_json_schema(value, schema):
20-
try:
21-
validate(value, schema)
22-
except JsonValidationError as e:
23-
raise ValidationError(str(e))
24-
25-
return value
26-
27-
2817
class BaseActionValidator(CamelSnakeModelSerializer[T], Generic[T]):
2918
data: Any = serializers.JSONField()
3019
config: Any = serializers.JSONField()
20+
type = serializers.ChoiceField(choices=[(t.value, t.name) for t in Action.Type])
21+
integration_id = serializers.IntegerField()
3122

3223
class Meta:
3324
model = T
34-
fields = ["config", "data", "integration_id", "type"]
3525

3626
def _get_action_handler(self) -> ActionHandler:
37-
initial_type = self.initial_data.get("type")
38-
action_type = self.validate_type(initial_type)
27+
action_type = self.initial_data.get("type")
3928
return action_handler_registry.get(action_type)
4029

41-
def validate_type(self, value) -> Action.Type:
42-
if not value:
43-
raise ValidationError("Action type is required")
44-
45-
try:
46-
Action.Type(value)
47-
except ValueError:
48-
raise ValidationError("Invalid action type")
49-
50-
return value
51-
5230
def validate_data(self, value) -> ActionData:
5331
data_schema = self._get_action_handler().data_schema
5432
return validate_json_schema(value, data_schema)
Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,77 @@
1+
from abc import abstractmethod
2+
from typing import Any, Generic, TypeVar
3+
4+
from jsonschema import ValidationError as JsonValidationError
15
from rest_framework import serializers
2-
from rest_framework.fields import Field
36

47
from sentry.api.serializers.rest_framework import CamelSnakeSerializer
8+
from sentry.utils.registry import NoRegistrationExistsError
9+
from sentry.workflow_engine.endpoints.validators.utils import validate_json_schema
10+
from sentry.workflow_engine.models.data_condition import CONDITION_OPS, Condition
11+
from sentry.workflow_engine.registry import condition_handler_registry
12+
from sentry.workflow_engine.types import DataConditionHandler
13+
14+
ComparisonType = TypeVar("ComparisonType")
15+
ConditionResult = TypeVar("ConditionResult")
16+
17+
18+
class AbstractDataConditionValidator(
19+
CamelSnakeSerializer,
20+
Generic[ComparisonType, ConditionResult],
21+
):
22+
id = serializers.IntegerField(required=False)
23+
type = serializers.ChoiceField(choices=[(t.value, t.value) for t in Condition])
24+
comparison = serializers.JSONField(required=True)
25+
condition_result = serializers.JSONField(required=True)
26+
condition_group_id = serializers.IntegerField(required=True)
27+
28+
@abstractmethod
29+
def validate_comparison(self, value: Any) -> ComparisonType:
30+
pass
31+
32+
@abstractmethod
33+
def validate_condition_result(self, value: Any) -> ConditionResult:
34+
pass
35+
536

37+
class BaseDataConditionValidator(
38+
AbstractDataConditionValidator[Any, Any],
39+
):
40+
def _get_handler(self) -> DataConditionHandler | None:
41+
condition_type = self.initial_data.get("type")
42+
if condition_type in CONDITION_OPS:
43+
return None
644

7-
class BaseDataConditionValidator(CamelSnakeSerializer):
8-
type = serializers.CharField(
9-
required=True,
10-
max_length=200,
11-
help_text="Condition used to compare data value to the stored comparison value",
12-
)
45+
try:
46+
return condition_handler_registry.get(condition_type)
47+
except NoRegistrationExistsError:
48+
raise serializers.ValidationError(f"Invalid condition type: {condition_type}")
1349

14-
@property
15-
def comparison(self) -> Field:
16-
raise NotImplementedError
50+
def validate_comparison(self, value: Any) -> Any:
51+
handler = self._get_handler()
1752

18-
@property
19-
def result(self) -> Field:
20-
raise NotImplementedError
53+
if not handler:
54+
raise serializers.ValidationError(
55+
"Condition Operators should implement their own validators for comparison"
56+
)
2157

22-
def validate(self, attrs):
23-
attrs = super().validate(attrs)
24-
return attrs
58+
try:
59+
return validate_json_schema(value, handler.comparison_json_schema)
60+
except JsonValidationError:
61+
raise serializers.ValidationError(
62+
f"Value, {value} does not match JSON Schema for comparison"
63+
)
2564

65+
def validate_condition_result(self, value: Any) -> Any:
66+
handler = self._get_handler()
67+
if not handler:
68+
raise serializers.ValidationError(
69+
"Condition Operators should implement their own validation for condition_result"
70+
)
2671

27-
class BaseDataConditionGroupValidator(CamelSnakeSerializer):
28-
logic_type = serializers.CharField(required=True)
29-
organization_id = serializers.IntegerField(required=True)
30-
conditions = BaseDataConditionValidator(many=True)
72+
try:
73+
return validate_json_schema(value, handler.condition_result_schema)
74+
except JsonValidationError:
75+
raise serializers.ValidationError(
76+
f"Value, {value}, does not match JSON Schema for condition result"
77+
)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from rest_framework import serializers
2+
3+
from sentry.api.serializers.rest_framework import CamelSnakeSerializer
4+
from sentry.workflow_engine.models import DataConditionGroup
5+
6+
7+
class BaseDataConditionGroupValidator(CamelSnakeSerializer):
8+
logic_type = serializers.ChoiceField([(t.value, t.value) for t in DataConditionGroup.Type])
9+
organization_id = serializers.IntegerField(required=True)
10+
conditions = serializers.ListField(required=False)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.forms import ValidationError
2+
from jsonschema import ValidationError as JsonValidationError
3+
from jsonschema import validate
4+
5+
6+
def validate_json_schema(value, schema):
7+
try:
8+
validate(value, schema)
9+
except JsonValidationError as e:
10+
raise ValidationError(str(e))
11+
12+
return value

src/sentry/workflow_engine/models/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"ActionGroupStatus",
55
"AlertRuleDetector",
66
"AlertRuleWorkflow",
7+
"Condition",
78
"DataCondition",
89
"DataConditionGroup",
910
"DataConditionGroupAction",
@@ -23,7 +24,7 @@
2324
from .action_group_status import ActionGroupStatus
2425
from .alertrule_detector import AlertRuleDetector
2526
from .alertrule_workflow import AlertRuleWorkflow
26-
from .data_condition import DataCondition
27+
from .data_condition import Condition, DataCondition
2728
from .data_condition_group import DataConditionGroup
2829
from .data_condition_group_action import DataConditionGroupAction
2930
from .data_source import DataPacket, DataSource

src/sentry/workflow_engine/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class Subgroup(StrEnum):
8484
group: ClassVar[Group]
8585
subgroup: ClassVar[Subgroup]
8686
comparison_json_schema: ClassVar[dict[str, Any]] = {}
87+
condition_result_schema: ClassVar[dict[str, Any]] = {}
8788

8889
@staticmethod
8990
def evaluate_value(value: T, comparison: Any) -> DataConditionResult:

0 commit comments

Comments
 (0)