Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
de51cf1
Codemod return types for literal returns (#97988)
thetruecpaul Aug 18, 2025
21be00c
type middleware/integrations/ (#97992)
thetruecpaul Aug 18, 2025
a9e9f42
type two ezpz files (#98000)
thetruecpaul Aug 18, 2025
2d36c58
fix(typing): add typing to demo_mode module (#97987)
isabellaenriquez Aug 18, 2025
2c79996
Type tests/sentry_plugins (#97998)
thetruecpaul Aug 18, 2025
2239a57
add misc files (#98007)
thetruecpaul Aug 18, 2025
a8ee9db
type tests/apidocs (#98012)
thetruecpaul Aug 18, 2025
799ea26
type src/sentry/endpoints/release_thresholds/ (#98025)
shayna-ch Aug 19, 2025
b0566e4
type tests/acceptance (#98011)
thetruecpaul Aug 19, 2025
ba57d54
Add types to some low-lift files (#98027)
thetruecpaul Aug 19, 2025
569b409
added types to src/sentry/api/bases/ (#98034)
shayna-ch Aug 20, 2025
c7b19bc
added types for src/sentry/dashboards (#98063)
shayna-ch Aug 20, 2025
438c371
codemod returns to `setup` and `teardown` (#98065)
thetruecpaul Aug 20, 2025
7e32d9d
fix(typing): dynamic sampling module (#98049)
isabellaenriquez Aug 20, 2025
3cbb696
added types for src/sentry/api/endpoints/organization_members (#98055)
shayna-ch Aug 20, 2025
607d1b6
improve plugin tests to not need explicit mocks (#98032)
thetruecpaul Aug 20, 2025
b993037
type 65 additional low-lift files (#98083)
thetruecpaul Aug 20, 2025
666790f
fix(typing): auth module (#98020)
isabellaenriquez Aug 21, 2025
5fd86b9
fix(typing): mail module (#98093)
isabellaenriquez Aug 21, 2025
1335b7c
type src/sentry/api/helpers (#98013)
thetruecpaul Aug 21, 2025
5f343e0
conftests (#98106)
thetruecpaul Aug 21, 2025
9c5a41a
fix(typing): Type pagerduty module (#98075)
Christinarlong Aug 21, 2025
437bca6
fix(typing): data_export module (#98096)
isabellaenriquez Aug 21, 2025
103663c
parameterized types (#98115)
thetruecpaul Aug 21, 2025
cd64ff9
type tests.sentry.incidents.action_handlers.* (#98112)
thetruecpaul Aug 21, 2025
6312178
expand strictlist mypy no errors (#98118)
thetruecpaul Aug 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion fixtures/integrations/jira/stub_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def get_transitions(self, issue_key):
def transition_issue(self, issue_key, transition_id):
pass

def user_id_field(self):
def user_id_field(self) -> str:
return "accountId"

def get_user(self, user_id):
Expand Down
161 changes: 124 additions & 37 deletions pyproject.toml

Large diffs are not rendered by default.

19 changes: 12 additions & 7 deletions src/sentry/api/bases/avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@

from sentry.api.fields import AvatarField
from sentry.api.serializers import serialize
from sentry.db.models.base import Model
from sentry.models.avatars.base import AvatarBase
from sentry.models.avatars.control_base import ControlAvatarBase

AvatarT = TypeVar("AvatarT", bound=AvatarBase)


class AvatarSerializer(serializers.Serializer):
class AvatarSerializer(serializers.Serializer[dict[str, Any]]):
avatar_photo = AvatarField(required=False)
avatar_type = serializers.ChoiceField(
choices=(("upload", "upload"), ("gravatar", "gravatar"), ("letter_avatar", "letter_avatar"))
)

def validate(self, attrs):
def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
attrs = super().validate(attrs)
if attrs.get("avatar_type") == "upload":
model_type = self.context["type"]
Expand All @@ -46,7 +47,7 @@ def validate(self, attrs):

class AvatarMixin(Generic[AvatarT]):
object_type: ClassVar[str]
serializer_cls: ClassVar[type[serializers.Serializer]] = AvatarSerializer
serializer_cls: ClassVar[type[serializers.Serializer[dict[str, Any]]]] = AvatarSerializer

@property
def model(self) -> type[AvatarT]:
Expand All @@ -56,21 +57,25 @@ def get(self, request: Request, **kwargs: Any) -> Response:
obj = kwargs.pop(self.object_type, None)
return Response(serialize(obj, request.user, **kwargs))

def get_serializer_context(self, obj, **kwargs: Any):
def get_serializer_context(self, obj: Model, **kwargs: Any) -> dict[str, Any]:
return {"type": self.model, "kwargs": {self.object_type: obj}}

def get_avatar_filename(self, obj):
def get_avatar_filename(self, obj: Model) -> str:
return f"{obj.id}.png"

def parse(self, request: Request, **kwargs: Any) -> tuple[Any, serializers.Serializer]:
def parse(
self, request: Request, **kwargs: Any
) -> tuple[Model, serializers.Serializer[dict[str, Any]]]:
obj = kwargs.pop(self.object_type, None)

serializer = self.serializer_cls(
data=request.data, context=self.get_serializer_context(obj)
)
return (obj, serializer)

def save_avatar(self, obj: Any, serializer: serializers.Serializer, **kwargs: Any) -> AvatarT:
def save_avatar(
self, obj: Model, serializer: serializers.Serializer[dict[str, Any]], **kwargs: Any
) -> AvatarT:
result = serializer.validated_data

return self.model.save_avatar(
Expand Down
27 changes: 19 additions & 8 deletions src/sentry/api/bases/group.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
from __future__ import annotations

import logging
from typing import Any

import sentry_sdk
from django.db.models import QuerySet
from rest_framework.permissions import SAFE_METHODS
from rest_framework.request import Request
from rest_framework.views import APIView

from sentry.api.api_owners import ApiOwner
from sentry.api.base import Endpoint
from sentry.api.bases.project import ProjectPermission
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.demo_mode.utils import is_demo_mode_enabled, is_demo_user
from sentry.integrations.tasks import create_comment, update_comment
from sentry.models.activity import Activity
from sentry.models.group import Group, GroupStatus, get_group_with_redirect
from sentry.models.grouplink import GroupLink
from sentry.models.organization import Organization
Expand All @@ -34,7 +38,8 @@ class GroupPermission(ProjectPermission):
"DELETE": ["event:admin"],
}

def has_object_permission(self, request: Request, view, group):
def has_object_permission(self, request: Request, view: APIView, group: Any) -> bool:
assert isinstance(group, Group)
return super().has_object_permission(request, view, group.project)


Expand All @@ -43,8 +48,13 @@ class GroupEndpoint(Endpoint):
permission_classes = (GroupPermission,)

def convert_args(
self, request: Request, issue_id, organization_id_or_slug=None, *args, **kwargs
):
self,
request: Request,
issue_id: str,
organization_id_or_slug: str | None = None,
*args: Any,
**kwargs: Any,
) -> tuple[tuple[Any, ...], dict[str, Any]]:
# TODO(tkaemming): Ideally, this would return a 302 response, rather
# than just returning the data that is bound to the new group. (It
# technically shouldn't be a 301, since the response could change again
Expand Down Expand Up @@ -96,12 +106,12 @@ def convert_args(

return (args, kwargs)

def get_external_issue_ids(self, group):
def get_external_issue_ids(self, group: Group) -> QuerySet[Any]:
return GroupLink.objects.filter(
project_id=group.project_id, group_id=group.id, linked_type=GroupLink.LinkedType.issue
).values_list("linked_id", flat=True)

def create_external_comment(self, request: Request, group, group_note):
def create_external_comment(self, request: Request, group: Group, group_note: Activity) -> None:
for external_issue_id in self.get_external_issue_ids(group):
create_comment.apply_async(
kwargs={
Expand All @@ -111,7 +121,7 @@ def create_external_comment(self, request: Request, group, group_note):
}
)

def update_external_comment(self, request: Request, group, group_note):
def update_external_comment(self, request: Request, group: Group, group_note: Activity) -> None:
for external_issue_id in self.get_external_issue_ids(group):
update_comment.apply_async(
kwargs={
Expand All @@ -133,15 +143,16 @@ class GroupAiPermission(GroupPermission):
# We want to allow POST requests in order to showcase AI features in demo mode
ALLOWED_METHODS = tuple(list(SAFE_METHODS) + ["POST"])

def has_permission(self, request: Request, view) -> bool:
def has_permission(self, request: Request, view: APIView) -> bool:
if is_demo_user(request.user):
if not is_demo_mode_enabled() or request.method not in self.ALLOWED_METHODS:
return False

return True
return super().has_permission(request, view)

def has_object_permission(self, request: Request, view, group) -> bool:
def has_object_permission(self, request: Request, view: APIView, group: Any) -> bool:
assert isinstance(group, Group)
if is_demo_user(request.user):
if not is_demo_mode_enabled() or request.method not in self.ALLOWED_METHODS:
return False
Expand Down
10 changes: 9 additions & 1 deletion src/sentry/api/bases/incident.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any

from rest_framework.exceptions import PermissionDenied
from rest_framework.request import Request

Expand All @@ -24,7 +26,13 @@ class IncidentPermission(OrganizationPermission):


class IncidentEndpoint(OrganizationEndpoint):
def convert_args(self, request: Request, incident_identifier, *args, **kwargs):
def convert_args(
self,
request: Request,
incident_identifier: str,
*args: Any,
**kwargs: Any,
) -> tuple[tuple[Any, ...], dict[str, Any]]:
args, kwargs = super().convert_args(request, *args, **kwargs)
organization = kwargs["organization"]

Expand Down
41 changes: 28 additions & 13 deletions src/sentry/api/bases/organization_events.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from __future__ import annotations

import itertools
from collections.abc import Callable, Sequence
from collections.abc import Callable, Iterable, Sequence
from datetime import timedelta
from typing import Any, cast
from urllib.parse import quote as urlquote

import sentry_sdk
from django.contrib.auth.models import AnonymousUser
from django.http.request import HttpRequest
from django.utils import timezone
from rest_framework.exceptions import ParseError, ValidationError
Expand All @@ -27,9 +28,9 @@
from sentry.api.serializers.snuba import SnubaTSResultSerializer
from sentry.api.utils import handle_query_errors
from sentry.discover.arithmetic import is_equation, strip_equation
from sentry.discover.models import DatasetSourcesTypes, DiscoverSavedQueryTypes
from sentry.discover.models import DatasetSourcesTypes, DiscoverSavedQuery, DiscoverSavedQueryTypes
from sentry.exceptions import InvalidSearchQuery
from sentry.models.dashboard_widget import DashboardWidgetTypes
from sentry.models.dashboard_widget import DashboardWidget, DashboardWidgetTypes
from sentry.models.dashboard_widget import DatasetSourcesTypes as DashboardDatasetSourcesTypes
from sentry.models.group import Group
from sentry.models.organization import Organization
Expand All @@ -43,6 +44,7 @@
from sentry.snuba.dataset import Dataset
from sentry.snuba.metrics.extraction import MetricSpecType
from sentry.snuba.utils import DATASET_LABELS, DATASET_OPTIONS, get_dataset
from sentry.users.models.user import User
from sentry.users.services.user.serial import serialize_generic_user
from sentry.utils import snuba
from sentry.utils.cursors import Cursor
Expand All @@ -51,7 +53,7 @@
from sentry.utils.snuba import MAX_FIELDS, SnubaTSResult


def get_query_columns(columns, rollup):
def get_query_columns(columns: list[str], rollup: int) -> list[str]:
"""
Backwards compatibility for incidents which uses the old
column aliases as it straddles both versions of events/discover.
Expand Down Expand Up @@ -113,7 +115,7 @@ def get_teams(self, request: Request, organization: Organization) -> list[Team]:
if not request.user:
return []

teams = get_teams(request, organization)
teams: Iterable[Team] = get_teams(request, organization)
if not teams:
teams = Team.objects.get_for_user(organization, request.user)

Expand Down Expand Up @@ -249,7 +251,14 @@ def handle_on_demand(self, request: Request) -> tuple[bool, MetricSpecType]:

return use_on_demand_metrics, on_demand_metric_type

def save_split_decision(self, widget, has_errors, has_transactions_data, organization, user):
def save_split_decision(
self,
widget: DashboardWidget,
has_errors: bool,
has_transactions_data: bool,
organization: Organization,
user: User | AnonymousUser,
) -> int | None:
"""This can be removed once the discover dataset has been fully split"""
source = DashboardDatasetSourcesTypes.INFERRED.value
if has_errors and not has_transactions_data:
Expand All @@ -273,15 +282,19 @@ def save_split_decision(self, widget, has_errors, has_transactions_data, organiz
return decision

def save_discover_saved_query_split_decision(
self, query, dataset_inferred_from_query, has_errors, has_transactions_data
):
self,
query: DiscoverSavedQuery,
dataset_inferred_from_query: int | None,
has_errors: bool,
has_transactions_data: bool,
) -> int | None:
"""
This can be removed once the discover dataset has been fully split.
If dataset is ambiguous (i.e., could be either transactions or errors),
default to errors.
"""
dataset_source = DatasetSourcesTypes.INFERRED.value
if dataset_inferred_from_query:
if dataset_inferred_from_query is not None:
decision = dataset_inferred_from_query
sentry_sdk.set_tag("discover.split_reason", "inferred_from_query")
elif has_errors and not has_transactions_data:
Expand Down Expand Up @@ -314,7 +327,7 @@ def handle_unit_meta(
units[key], meta[key] = self.get_unit_and_type(key, value)
return meta, units

def get_unit_and_type(self, field, field_type):
def get_unit_and_type(self, field: str, field_type: str) -> tuple[str | None, str]:
if field_type in SIZE_UNITS:
return field_type, "size"
elif field_type in DURATION_UNITS:
Expand Down Expand Up @@ -427,7 +440,7 @@ def handle_data(

return results

def handle_error_upsampling(self, project_ids: Sequence[int], results: dict[str, Any]):
def handle_error_upsampling(self, project_ids: Sequence[int], results: dict[str, Any]) -> None:
"""
If the query is for error upsampled projects, we convert various functions under the hood.
We need to rename these fields before returning the results to the client, to hide the conversion.
Expand Down Expand Up @@ -704,7 +717,9 @@ def serialize_multiple_axis(

return result

def update_meta_with_accuracy(self, meta, event_result, query_column) -> None:
def update_meta_with_accuracy(
self, meta: dict[str, Any], event_result: SnubaTSResult, query_column: str
) -> None:
if "processed_timeseries" in event_result.data:
processed_timeseries = event_result.data["processed_timeseries"]
meta["accuracy"] = {
Expand All @@ -724,7 +739,7 @@ def serialize_accuracy_data(
data: Any,
column: str,
null_zero: bool = False,
):
) -> list[dict[str, Any]]:
serialized_values = []
for timestamp, group in itertools.groupby(data, key=lambda r: r["time"]):
for row in group:
Expand Down
6 changes: 5 additions & 1 deletion src/sentry/api/bases/organization_flag.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import Any

from rest_framework.request import Request

from sentry import features
Expand All @@ -22,7 +24,9 @@ def feature_flags(self) -> list[str]:
"Requires set 'feature_flags' property to restrict this endpoint."
)

def convert_args(self, request: Request, *args, **kwargs):
def convert_args(
self, request: Request, *args: Any, **kwargs: Any
) -> tuple[tuple[Any, ...], dict[str, Any]]:
parsed_args, parsed_kwargs = super().convert_args(request, *args, **kwargs)
organization = parsed_kwargs.get("organization")
feature_gate = [
Expand Down
6 changes: 3 additions & 3 deletions src/sentry/api/bases/organizationmember.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ class MemberIdField(serializers.IntegerField):
Allow "me" in addition to integers
"""

def to_internal_value(self, data):
def to_internal_value(self, data: float | int | str) -> Any:
if data == "me":
return data
return super().to_internal_value(data)

def run_validation(self, data=empty):
def run_validation(self, data: object | None = empty) -> object | None:
if data == "me":
return data
return super().run_validation(data)


class MemberSerializer(serializers.Serializer):
class MemberSerializer(serializers.Serializer[dict[str, int | Literal["me"]]]):
id = MemberIdField(min_value=0, max_value=BoundedAutoField.MAX_VALUE, required=True)


Expand Down
12 changes: 7 additions & 5 deletions src/sentry/api/bases/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ class ProjectEndpoint(Endpoint):
def convert_args(
self,
request: Request,
*args,
**kwargs,
):
*args: Any,
**kwargs: Any,
) -> tuple[tuple[Any, ...], dict[str, Any]]:
if args and args[0] is not None:
organization_id_or_slug: int | str = args[0]
# Required so it behaves like the original convert_args, where organization_id_or_slug was another parameter
Expand Down Expand Up @@ -193,7 +193,9 @@ def convert_args(
kwargs["project"] = project
return (args, kwargs)

def get_filter_params(self, request: Request, project, date_filter_optional=False):
def get_filter_params(
self, request: Request, project: Project, date_filter_optional: bool = False
) -> dict[str, Any]:
"""Similar to the version on the organization just for a single project."""
# get the top level params -- projects, time range, and environment
# from the request
Expand All @@ -203,7 +205,7 @@ def get_filter_params(self, request: Request, project, date_filter_optional=Fals
raise ProjectEventsError(str(e))

environments = [env.name for env in get_environments(request, project.organization)]
params = {"start": start, "end": end, "project_id": [project.id]}
params: dict[str, Any] = {"start": start, "end": end, "project_id": [project.id]}
if environments:
params["environment"] = environments

Expand Down
Loading
Loading