Skip to content

Commit 1730d73

Browse files
authored
chore(typing): Add condition typeddict to processing (#70006)
Taken from #68517 and added as a followup PR alongside #69068
1 parent b96af13 commit 1730d73

File tree

9 files changed

+64
-22
lines changed

9 files changed

+64
-22
lines changed

src/sentry/api/endpoints/project_agnostic_rule_conditions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def get(self, request: Request, organization) -> Response:
2222

2323
def info_extractor(rule_cls):
2424
context = {"id": rule_cls.id, "label": rule_cls.label}
25-
node = rule_cls(None)
25+
node = rule_cls(project=None)
2626
if hasattr(node, "form_fields"):
2727
context["formFields"] = node.form_fields
2828

src/sentry/api/serializers/models/rule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def generate_rule_label(project, rule, data):
2020
if rule_cls is None:
2121
return
2222

23-
rule_inst = rule_cls(project, data=data, rule=rule)
23+
rule_inst = rule_cls(project=project, data=data, rule=rule)
2424
return rule_inst.render_label()
2525

2626

src/sentry/api/serializers/rest_framework/rule.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def to_internal_value(self, data):
4646
msg = "Invalid node. Could not find '%s'"
4747
raise ValidationError(msg % data["id"])
4848

49-
node = cls(self.context["project"], data)
49+
node = cls(project=self.context["project"], data=data)
5050

5151
# Nodes with user-declared fields will manage their own validation
5252
if node.id in SENTRY_APP_ACTIONS:

src/sentry/rules/conditions/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import abc
22
from collections.abc import Sequence
33
from datetime import datetime
4+
from typing import TypedDict
45

56
from sentry.eventstore.models import GroupEvent
67
from sentry.rules.base import EventState, RuleBase
78
from sentry.types.condition_activity import ConditionActivity
89

910

11+
class GenericCondition(TypedDict):
12+
# the ID in the rules registry that maps to a condition class
13+
# e.g. "sentry.rules.conditions.every_event.EveryEventCondition"
14+
id: str
15+
16+
1017
class EventCondition(RuleBase, abc.ABC):
1118
rule_type = "condition/event"
1219

src/sentry/rules/conditions/event_frequency.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections import defaultdict
77
from collections.abc import Callable, Mapping
88
from datetime import datetime, timedelta
9-
from typing import Any
9+
from typing import Any, Literal, NotRequired
1010

1111
from django import forms
1212
from django.core.cache import cache
@@ -19,7 +19,7 @@
1919
from sentry.issues.grouptype import GroupCategory
2020
from sentry.models.group import Group
2121
from sentry.rules import EventState
22-
from sentry.rules.conditions.base import EventCondition
22+
from sentry.rules.conditions.base import EventCondition, GenericCondition
2323
from sentry.tsdb.base import TSDBModel
2424
from sentry.types.condition_activity import (
2525
FREQUENCY_CONDITION_BUCKET_SIZE,
@@ -55,6 +55,26 @@ class ComparisonType(TextChoices):
5555
PERCENT = "percent"
5656

5757

58+
class EventFrequencyConditionData(GenericCondition):
59+
"""
60+
The base typed dict for all condition data representing EventFrequency issue
61+
alert rule conditions
62+
"""
63+
64+
# Either the count or percentage.
65+
value: int
66+
# The interval to compare the value against such as 5m, 1h, 3w, etc.
67+
# e.g. # of issues is more than {value} in {interval}.
68+
interval: str
69+
# NOTE: Some of tne earliest COUNT conditions were created without the
70+
# comparisonType field, although modern rules will always have it.
71+
comparisonType: NotRequired[Literal[ComparisonType.COUNT, ComparisonType.PERCENT]]
72+
# The previous interval to compare the curr interval against. This is only
73+
# present in PERCENT conditions.
74+
# e.g. # of issues is 50% higher in {interval} compared to {comparisonInterval}
75+
comparisonInterval: NotRequired[str]
76+
77+
5878
class EventFrequencyForm(forms.Form):
5979
intervals = STANDARD_INTERVALS
6080
interval = forms.ChoiceField(
@@ -102,6 +122,9 @@ class BaseEventFrequencyCondition(EventCondition, abc.ABC):
102122

103123
def __init__(
104124
self,
125+
# Data specifically takes on this typeddict form for the
126+
# Event Frequency condition classes.
127+
data: EventFrequencyConditionData | None = None,
105128
*args: Any,
106129
**kwargs: Any,
107130
) -> None:
@@ -119,6 +142,7 @@ def __init__(
119142
],
120143
},
121144
}
145+
kwargs["data"] = data
122146

123147
super().__init__(*args, **kwargs)
124148

src/sentry/rules/history/preview.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def get_issue_state_activity(
145145
if condition_cls is None:
146146
raise PreviewException
147147
# instantiates a EventCondition subclass and retrieves activities related to it
148-
condition_inst = condition_cls(project, data=condition)
148+
condition_inst = condition_cls(project=project, data=condition)
149149
try:
150150
activities = condition_inst.get_activity(start, end, CONDITION_ACTIVITY_LIMIT)
151151
for activity in activities:
@@ -403,7 +403,9 @@ def apply_frequency_conditions(
403403
condition_cls = rules.get(condition_data["id"])
404404
if condition_cls is None:
405405
raise PreviewException
406-
condition_types[condition_data["id"]].append(condition_cls(project, data=condition_data))
406+
condition_types[condition_data["id"]].append(
407+
condition_cls(project=project, data=condition_data)
408+
)
407409

408410
filtered_activity = defaultdict(list)
409411
if condition_match == "all":

src/sentry/rules/processing/delayed_processing.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import logging
22
from collections import defaultdict
3-
from collections.abc import MutableMapping
4-
from typing import Any, DefaultDict, NamedTuple
3+
from typing import DefaultDict, NamedTuple
54

65
from sentry.buffer.redis import BufferHookEvent, RedisBuffer, redis_buffer_registry
76
from sentry.models.project import Project
87
from sentry.models.rule import Rule
98
from sentry.rules import rules
10-
from sentry.rules.conditions.event_frequency import BaseEventFrequencyCondition, ComparisonType
9+
from sentry.rules.conditions.event_frequency import (
10+
BaseEventFrequencyCondition,
11+
ComparisonType,
12+
EventFrequencyConditionData,
13+
)
1114
from sentry.rules.processing.processor import is_condition_slow, split_conditions_and_filters
1215
from sentry.silo.base import SiloMode
1316
from sentry.tasks.base import instrumented_task
@@ -30,24 +33,24 @@ def __repr__(self):
3033

3134

3235
class DataAndGroups(NamedTuple):
33-
data: MutableMapping[str, Any] | None
36+
data: EventFrequencyConditionData | None
3437
group_ids: set[int]
3538

3639
def __repr__(self):
3740
return f"data: {self.data}\ngroup_ids: {self.group_ids}"
3841

3942

40-
def get_slow_conditions(rule: Rule) -> list[MutableMapping[str, str]]:
43+
def get_slow_conditions(rule: Rule) -> list[EventFrequencyConditionData]:
4144
"""
4245
Returns the slow conditions of a rule model instance.
4346
"""
4447
conditions_and_filters = rule.data.get("conditions", ())
4548
conditions, _ = split_conditions_and_filters(conditions_and_filters)
46-
slow_conditions: list[MutableMapping[str, str]] = [
47-
cond for cond in conditions if is_condition_slow(cond)
48-
]
49+
slow_conditions = [cond for cond in conditions if is_condition_slow(cond)]
4950

50-
return slow_conditions
51+
# MyPy refuses to make TypedDict compatible with MutableMapping
52+
# https://github.com/python/mypy/issues/4976
53+
return slow_conditions # type: ignore[return-value]
5154

5255

5356
def get_rules_to_groups(rulegroup_to_events: dict[str, str]) -> DefaultDict[int, set[int]]:
@@ -61,8 +64,10 @@ def get_rules_to_groups(rulegroup_to_events: dict[str, str]) -> DefaultDict[int,
6164

6265
def get_rule_to_slow_conditions(
6366
alert_rules: list[Rule],
64-
) -> DefaultDict[Rule, list[MutableMapping[str, str]]]:
65-
rule_to_slow_conditions: DefaultDict[Rule, list[MutableMapping[str, str]]] = defaultdict(list)
67+
) -> DefaultDict[Rule, list[EventFrequencyConditionData]]:
68+
rule_to_slow_conditions: DefaultDict[Rule, list[EventFrequencyConditionData]] = defaultdict(
69+
list
70+
)
6671
for rule in alert_rules:
6772
slow_conditions = get_slow_conditions(rule)
6873
for condition_data in slow_conditions:
@@ -113,7 +118,9 @@ def get_condition_group_results(
113118
logger.warning("Unregistered condition %r", unique_condition.cls_id)
114119
return None
115120

116-
condition_inst = condition_cls(project=project, data=condition_data)
121+
# MyPy refuses to make TypedDict compatible with MutableMapping
122+
# https://github.com/python/mypy/issues/4976
123+
condition_inst = condition_cls(project=project, data=condition_data) # type: ignore[arg-type]
117124
if not isinstance(condition_inst, BaseEventFrequencyCondition):
118125
logger.warning("Unregistered condition %r", condition_cls.id)
119126
return None
@@ -139,7 +146,7 @@ def get_condition_group_results(
139146

140147
def get_rules_to_fire(
141148
condition_group_results: dict[UniqueCondition, dict[int, int]],
142-
rule_to_slow_conditions: DefaultDict[Rule, list[MutableMapping[str, str]]],
149+
rule_to_slow_conditions: DefaultDict[Rule, list[EventFrequencyConditionData]],
143150
rules_to_groups: DefaultDict[int, set[int]],
144151
) -> DefaultDict[Rule, set[int]]:
145152
rules_to_fire = defaultdict(set)

src/sentry/rules/processing/processor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ def get_rule_type(condition: Mapping[str, Any]) -> str | None:
6464
return rule_type
6565

6666

67-
def split_conditions_and_filters(rule_condition_list):
67+
def split_conditions_and_filters(
68+
rule_condition_list,
69+
) -> tuple[list[MutableMapping[str, Any]], list[MutableMapping[str, Any]]]:
6870
condition_list = []
6971
filter_list = []
7072
for rule_cond in rule_condition_list:

tests/sentry/rules/history/endpoints/test_project_rule_preview.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def test_inbox_reason(self):
128128
frequency=10,
129129
)
130130

131-
for (group, reason) in group_reason:
131+
for group, reason in group_reason:
132132
assert any([int(g["id"]) == group.id for g in resp.data])
133133

134134
for preview_group in resp.data:

0 commit comments

Comments
 (0)