diff --git a/fixtures/integrations/jira/stub_client.py b/fixtures/integrations/jira/stub_client.py index 9cebf81245c2bb..9544d0ea9908c6 100644 --- a/fixtures/integrations/jira/stub_client.py +++ b/fixtures/integrations/jira/stub_client.py @@ -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): diff --git a/pyproject.toml b/pyproject.toml index 2dee5af8731868..2ef8beb370ae44 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -307,9 +307,6 @@ module = [ "sentry.api.endpoints.organization_releases", "sentry.api.paginator", "sentry.db.postgres.base", - "sentry.integrations.pagerduty.actions.form", - "sentry.integrations.slack.message_builder.notifications.issues", - "sentry.integrations.slack.webhooks.event", "sentry.issues.search", "sentry.middleware.auth", "sentry.middleware.ratelimit", @@ -349,32 +346,26 @@ module = [ "fixtures.safe_migrations_apps.*", "fixtures.schema_validation", "sentry.analytics.*", + "sentry.api.bases.*", "sentry.api.decorators", "sentry.api.endpoints.integrations.sentry_apps.installation.external_issue.*", "sentry.api.endpoints.organization_events_spans_performance", + "sentry.api.endpoints.organization_member.*", "sentry.api.endpoints.project_repo_path_parsing", "sentry.api.endpoints.project_rules_configuration", - "sentry.api.endpoints.release_thresholds.health_checks.*", + "sentry.api.endpoints.release_thresholds.*", "sentry.api.event_search", - "sentry.api.helpers.deprecation", - "sentry.api.helpers.environments", - "sentry.api.helpers.error_upsampling", - "sentry.api.helpers.group_index.delete", - "sentry.api.helpers.group_index.update", - "sentry.api.helpers.source_map_helper", + "sentry.api.helpers.*", "sentry.api.permissions", "sentry.api.serializers.models.organization_member.*", "sentry.api.serializers.rest_framework.group_notes", "sentry.audit_log.services.*", - "sentry.auth.access", - "sentry.auth.authenticators.recovery_code", - "sentry.auth.manager", - "sentry.auth.services.*", - "sentry.auth.view", + "sentry.auth.*", "sentry.bgtasks.*", "sentry.buffer.*", "sentry.build.*", - "sentry.data_export.processors.issues_by_tag", + "sentry.dashboards.*", + "sentry.data_export.*", "sentry.data_secrecy.models.*", "sentry.data_secrecy.service.*", "sentry.db.models.fields.citext", @@ -386,12 +377,9 @@ module = [ "sentry.db.models.utils", "sentry.db.pending_deletion", "sentry.deletions.*", + "sentry.demo_mode.*", "sentry.digests.*", - "sentry.dynamic_sampling.models.*", - "sentry.dynamic_sampling.rules.biases.*", - "sentry.dynamic_sampling.rules.combinators.*", - "sentry.dynamic_sampling.rules.helpers.*", - "sentry.dynamic_sampling.tasks.helpers.*", + "sentry.dynamic_sampling.*", "sentry.eventstream.*", "sentry.eventtypes.error", "sentry.feedback.migrations.*", @@ -427,11 +415,14 @@ module = [ "sentry.integrations.jira_server.actions.*", "sentry.integrations.jira_server.utils.*", "sentry.integrations.models.integration_feature", + "sentry.integrations.pagerduty.*", "sentry.integrations.project_management.*", "sentry.integrations.repository.*", "sentry.integrations.services.*", + "sentry.integrations.slack.message_builder.notifications.issues", "sentry.integrations.slack.threads.*", "sentry.integrations.slack.views.*", + "sentry.integrations.slack.webhooks.event", "sentry.integrations.source_code_management.repository", "sentry.integrations.utils.sync", "sentry.integrations.vsts.actions.*", @@ -503,6 +494,8 @@ module = [ "sentry.issues.update_inbox", "sentry.lang.java.processing", "sentry.llm.*", + "sentry.mail.*", + "sentry.middleware.integrations.*", "sentry.middleware.reporting_endpoint", "sentry.migrations.*", "sentry.models.activity", @@ -655,6 +648,8 @@ module = [ "social_auth.admin", "social_auth.migrations.*", "sudo.*", + "tests.acceptance.*", + "tests.apidocs.*", "tests.sentry.api.endpoints.issues.*", "tests.sentry.api.endpoints.release_thresholds.utils.*", "tests.sentry.api.endpoints.secret_scanning.*", @@ -795,6 +790,7 @@ module = [ "tests.sentry.workflow_engine.endpoints.utils.*", "tests.sentry.workflow_engine.handlers.action.*", "tests.sentry.workflow_engine.models.*", + "tests.sentry_plugins.*", "tools.*", ] disallow_any_generics = true diff --git a/src/sentry/api/bases/avatar.py b/src/sentry/api/bases/avatar.py index ad7f823529a1ff..34dbcc7e51b432 100644 --- a/src/sentry/api/bases/avatar.py +++ b/src/sentry/api/bases/avatar.py @@ -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"] @@ -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]: @@ -56,13 +57,15 @@ 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( @@ -70,7 +73,9 @@ def parse(self, request: Request, **kwargs: Any) -> tuple[Any, serializers.Seria ) 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( diff --git a/src/sentry/api/bases/group.py b/src/sentry/api/bases/group.py index 48d5eee887acf9..b3efea5ae6f785 100644 --- a/src/sentry/api/bases/group.py +++ b/src/sentry/api/bases/group.py @@ -1,10 +1,13 @@ 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 @@ -12,6 +15,7 @@ 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 @@ -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) @@ -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 @@ -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={ @@ -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={ @@ -133,7 +143,7 @@ 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 @@ -141,7 +151,8 @@ def has_permission(self, request: Request, view) -> bool: 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 diff --git a/src/sentry/api/bases/incident.py b/src/sentry/api/bases/incident.py index aa18754573d9b3..27288d2f06f61e 100644 --- a/src/sentry/api/bases/incident.py +++ b/src/sentry/api/bases/incident.py @@ -1,3 +1,5 @@ +from typing import Any + from rest_framework.exceptions import PermissionDenied from rest_framework.request import Request @@ -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"] diff --git a/src/sentry/api/bases/organization_events.py b/src/sentry/api/bases/organization_events.py index e2dbe93ee6dceb..d00155618e4e2c 100644 --- a/src/sentry/api/bases/organization_events.py +++ b/src/sentry/api/bases/organization_events.py @@ -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 @@ -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 @@ -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 @@ -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. @@ -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) @@ -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: @@ -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: @@ -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: @@ -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. @@ -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"] = { @@ -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: diff --git a/src/sentry/api/bases/organization_flag.py b/src/sentry/api/bases/organization_flag.py index 122f3fcea2f98d..ae42a98b43718a 100644 --- a/src/sentry/api/bases/organization_flag.py +++ b/src/sentry/api/bases/organization_flag.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from rest_framework.request import Request from sentry import features @@ -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 = [ diff --git a/src/sentry/api/bases/organizationmember.py b/src/sentry/api/bases/organizationmember.py index 4e0b8fb4ce18cb..284b8fcf8eaac7 100644 --- a/src/sentry/api/bases/organizationmember.py +++ b/src/sentry/api/bases/organizationmember.py @@ -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) diff --git a/src/sentry/api/bases/project.py b/src/sentry/api/bases/project.py index c211b2b265f5af..39b9c6eda482ac 100644 --- a/src/sentry/api/bases/project.py +++ b/src/sentry/api/bases/project.py @@ -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 @@ -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 @@ -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 diff --git a/src/sentry/api/bases/team.py b/src/sentry/api/bases/team.py index 6f4701158bc535..cd73b066c631e4 100644 --- a/src/sentry/api/bases/team.py +++ b/src/sentry/api/bases/team.py @@ -1,5 +1,9 @@ +from collections.abc import Sequence +from typing import Any + from rest_framework.permissions import BasePermission from rest_framework.request import Request +from rest_framework.views import APIView from sentry.api.base import Endpoint from sentry.api.exceptions import ResourceDoesNotExist @@ -9,8 +13,8 @@ from .organization import OrganizationPermission -def has_team_permission(request, team, scope_map): - allowed_scopes = set(scope_map.get(request.method, [])) +def has_team_permission(request: Request, team: Team, scope_map: dict[str, Sequence[str]]) -> bool: + allowed_scopes = set(scope_map.get(request.method or "", [])) return any(request.access.has_team_scope(team, s) for s in allowed_scopes) @@ -22,7 +26,7 @@ class TeamPermission(OrganizationPermission): "DELETE": ["team:admin"], } - def has_object_permission(self, request: Request, view, team): + def has_object_permission(self, request: Request, view: APIView, team: Any) -> bool: has_org_scope = super().has_object_permission(request, view, team.organization) if has_org_scope: # Org-admin has "team:admin", but they can only act on their teams @@ -36,8 +40,13 @@ class TeamEndpoint(Endpoint): permission_classes: tuple[type[BasePermission], ...] = (TeamPermission,) def convert_args( - self, request: Request, organization_id_or_slug, team_id_or_slug, *args, **kwargs - ): + self, + request: Request, + organization_id_or_slug: str | int, + team_id_or_slug: str | int, + *args: Any, + **kwargs: Any, + ) -> tuple[tuple[Any, ...], dict[str, Any]]: try: team = ( Team.objects.filter( diff --git a/src/sentry/api/endpoints/organization_events_meta.py b/src/sentry/api/endpoints/organization_events_meta.py index 53825bd9398a0b..cf82f86b3c3574 100644 --- a/src/sentry/api/endpoints/organization_events_meta.py +++ b/src/sentry/api/endpoints/organization_events_meta.py @@ -21,8 +21,8 @@ from sentry.api.utils import handle_query_errors from sentry.middleware import is_frontend_request from sentry.models.organization import Organization -from sentry.search.eap.types import SearchResolverConfig -from sentry.search.events.types import SnubaParams +from sentry.search.eap.types import EAPResponse, SearchResolverConfig +from sentry.search.events.types import EventsResponse, SnubaParams from sentry.snuba import spans_indexed, spans_metrics from sentry.snuba.query_sources import QuerySource from sentry.snuba.referrer import Referrer @@ -217,7 +217,9 @@ def get(self, request: Request, organization: Organization) -> Response: ) -def get_span_samples(request: Request, snuba_params: SnubaParams, orderby: list[str] | None): +def get_span_samples( + request: Request, snuba_params: SnubaParams, orderby: list[str] | None +) -> EventsResponse: is_frontend = is_frontend_request(request) buckets = request.GET.get("intervals", 3) lower_bound = request.GET.get("lowerBound", 0) @@ -297,7 +299,9 @@ def get_span_samples(request: Request, snuba_params: SnubaParams, orderby: list[ ) -def get_eap_span_samples(request: Request, snuba_params: SnubaParams, orderby: list[str] | None): +def get_eap_span_samples( + request: Request, snuba_params: SnubaParams, orderby: list[str] | None +) -> EAPResponse: lower_bound = request.GET.get("lowerBound", 0) first_bound = request.GET.get("firstBound") second_bound = request.GET.get("secondBound") diff --git a/src/sentry/api/endpoints/organization_member/__init__.py b/src/sentry/api/endpoints/organization_member/__init__.py index 87c8e2886959c2..dee4f55c9e1581 100644 --- a/src/sentry/api/endpoints/organization_member/__init__.py +++ b/src/sentry/api/endpoints/organization_member/__init__.py @@ -24,7 +24,7 @@ def save_team_assignments( organization_member: OrganizationMember, teams: list[Team] | None, teams_with_roles: list[tuple[Team, str]] | None = None, -): +) -> None: # https://github.com/getsentry/sentry/pull/6054/files/8edbdb181cf898146eda76d46523a21d69ab0ec7#r145798271 lock = locks.get( f"org:member:{organization_member.id}", duration=5, name="save_team_assignment" diff --git a/src/sentry/api/endpoints/organization_member/index.py b/src/sentry/api/endpoints/organization_member/index.py index 8cd5f8a436576e..693eef18bd16fb 100644 --- a/src/sentry/api/endpoints/organization_member/index.py +++ b/src/sentry/api/endpoints/organization_member/index.py @@ -1,3 +1,5 @@ +from typing import Any + from django.conf import settings from django.db import router, transaction from django.db.models import Exists, F, OuterRef, Q @@ -45,7 +47,7 @@ @extend_schema_serializer( deprecate_fields=["role", "teams"], exclude_fields=["regenerate", "role", "teams"] ) -class OrganizationMemberRequestSerializer(serializers.Serializer): +class OrganizationMemberRequestSerializer(serializers.Serializer[dict[str, Any]]): email = AllowedEmailField( max_length=75, required=True, help_text="The email address to send the invitation to." ) @@ -82,7 +84,7 @@ class OrganizationMemberRequestSerializer(serializers.Serializer): ) regenerate = serializers.BooleanField(required=False) - def validate_email(self, email): + def validate_email(self, email: str) -> str: users = user_service.get_many_by_email( emails=[email], is_active=True, @@ -111,10 +113,10 @@ def validate_email(self, email): return email - def validate_role(self, role): + def validate_role(self, role: str) -> str: return self.validate_orgRole(role) - def validate_orgRole(self, role): + def validate_orgRole(self, role: str) -> str: if role == "billing" and features.has( "organizations:invite-billing", self.context["organization"] ): @@ -130,7 +132,7 @@ def validate_orgRole(self, role): ) return role - def validate_teams(self, teams): + def validate_teams(self, teams: list[Team]) -> list[Team]: valid_teams = list( Team.objects.filter( organization=self.context["organization"], status=TeamStatus.ACTIVE, slug__in=teams @@ -142,7 +144,7 @@ def validate_teams(self, teams): return valid_teams - def validate_teamRoles(self, teamRoles) -> list[tuple[Team, str]]: + def validate_teamRoles(self, teamRoles: list[dict[str, Any]]) -> list[tuple[Team, str]]: roles = {item["role"] for item in teamRoles} valid_roles = [r.id for r in team_roles.get_all()] + [None] if roles.difference(valid_roles): @@ -314,7 +316,7 @@ def get(self, request: Request, organization: Organization) -> Response: }, examples=OrganizationMemberExamples.CREATE_ORG_MEMBER, ) - def post(self, request: Request, organization) -> Response: + def post(self, request: Request, organization: Organization) -> Response: """ Add or invite a member to an organization. """ diff --git a/src/sentry/api/endpoints/organization_member/requests/invite/details.py b/src/sentry/api/endpoints/organization_member/requests/invite/details.py index 6cb2c2844ce94f..f25dbdfcc8a0c4 100644 --- a/src/sentry/api/endpoints/organization_member/requests/invite/details.py +++ b/src/sentry/api/endpoints/organization_member/requests/invite/details.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Literal +from typing import Any, Literal from rest_framework import serializers, status from rest_framework.request import Request @@ -23,10 +23,10 @@ from ... import get_allowed_org_roles, save_team_assignments -class ApproveInviteRequestSerializer(serializers.Serializer): +class ApproveInviteRequestSerializer(serializers.Serializer[dict[str, Any]]): approve = serializers.BooleanField(required=True, write_only=True) - def validate_approve(self, approve): + def validate_approve(self, approve: bool) -> bool: request = self.context["request"] member = self.context["member"] allowed_roles = self.context["allowed_roles"] diff --git a/src/sentry/api/endpoints/organization_member/requests/invite/index.py b/src/sentry/api/endpoints/organization_member/requests/invite/index.py index 2d29c8bf0d71d4..74abb24ec65626 100644 --- a/src/sentry/api/endpoints/organization_member/requests/invite/index.py +++ b/src/sentry/api/endpoints/organization_member/requests/invite/index.py @@ -55,7 +55,7 @@ def get(self, request: Request, organization: Organization) -> Response: paginator_cls=OffsetPaginator, ) - def post(self, request: Request, organization) -> Response: + def post(self, request: Request, organization: Organization) -> Response: """ Add a invite request to Organization ```````````````````````````````````` diff --git a/src/sentry/api/endpoints/organization_member/requests/join.py b/src/sentry/api/endpoints/organization_member/requests/join.py index 1088750f7c683b..929b003e3f5a06 100644 --- a/src/sentry/api/endpoints/organization_member/requests/join.py +++ b/src/sentry/api/endpoints/organization_member/requests/join.py @@ -1,4 +1,5 @@ import logging +from typing import Any from django.db import IntegrityError from django.db.models import Q @@ -13,6 +14,7 @@ from sentry.auth.services.auth import auth_service from sentry.demo_mode.utils import is_demo_user from sentry.hybridcloud.models.outbox import outbox_context +from sentry.models.organization import Organization from sentry.models.organizationmember import InviteStatus, OrganizationMember from sentry.notifications.notifications.organization_request import JoinRequestNotification from sentry.notifications.utils.tasks import async_send_notification @@ -23,11 +25,13 @@ logger = logging.getLogger(__name__) -class JoinRequestSerializer(serializers.Serializer): +class JoinRequestSerializer(serializers.Serializer[dict[str, Any]]): email = AllowedEmailField(max_length=75, required=True) -def create_organization_join_request(organization, email, ip_address=None): +def create_organization_join_request( + organization: Organization, email: str, ip_address: str | None = None +) -> OrganizationMember | None: with outbox_context(flush=False): om = OrganizationMember.objects.filter( Q(email__iexact=email) @@ -35,7 +39,7 @@ def create_organization_join_request(organization, email, ip_address=None): organization=organization, ).first() if om: - return + return None try: om = OrganizationMember.objects.create( @@ -66,7 +70,7 @@ class OrganizationJoinRequestEndpoint(OrganizationEndpoint): } } - def post(self, request: Request, organization) -> Response: + def post(self, request: Request, organization: Organization) -> Response: if organization.get_option("sentry:join_requests") is False: return Response( {"detail": "Your organization does not allow join requests."}, status=403 diff --git a/src/sentry/api/endpoints/organization_member/team_details.py b/src/sentry/api/endpoints/organization_member/team_details.py index 8fa639377cc6c7..f7a61bead5b47c 100644 --- a/src/sentry/api/endpoints/organization_member/team_details.py +++ b/src/sentry/api/endpoints/organization_member/team_details.py @@ -52,7 +52,7 @@ class OrganizationMemberTeamSerializerResponse(TypedDict): @extend_schema_serializer(exclude_fields=["isActive"]) -class OrganizationMemberTeamSerializer(serializers.Serializer): +class OrganizationMemberTeamSerializer(serializers.Serializer[dict[str, Any]]): isActive = serializers.BooleanField() teamRole = serializers.ChoiceField( choices=team_roles.get_descriptions(), diff --git a/src/sentry/api/endpoints/organization_member/utils.py b/src/sentry/api/endpoints/organization_member/utils.py index bc3107d5ddd6d4..d547b1b26d7f05 100644 --- a/src/sentry/api/endpoints/organization_member/utils.py +++ b/src/sentry/api/endpoints/organization_member/utils.py @@ -2,6 +2,11 @@ from rest_framework.request import Request from sentry.api.bases.organization import OrganizationPermission +from sentry.models.organization import Organization +from sentry.organizations.services.organization.model import ( + RpcOrganization, + RpcUserOrganizationContext, +) ERR_RATE_LIMITED = "You are being rate limited for too many invitations." @@ -50,5 +55,9 @@ class RelaxedMemberPermission(OrganizationPermission): # Allow deletions to happen for disabled members so they can remove themselves # allowing other methods should be fine as well even if we don't strictly need to allow them - def is_member_disabled_from_limit(self, request: Request, organization): + def is_member_disabled_from_limit( + self, + request: Request, + organization: RpcUserOrganizationContext | RpcOrganization | Organization | int, + ) -> bool: return False diff --git a/src/sentry/api/endpoints/organization_on_demand_metrics_estimation_stats.py b/src/sentry/api/endpoints/organization_on_demand_metrics_estimation_stats.py index cb2a9310a375e6..3fdce282d167de 100644 --- a/src/sentry/api/endpoints/organization_on_demand_metrics_estimation_stats.py +++ b/src/sentry/api/endpoints/organization_on_demand_metrics_estimation_stats.py @@ -1,5 +1,5 @@ -from collections.abc import Sequence -from datetime import datetime +from collections.abc import Callable, Sequence +from datetime import timedelta from enum import Enum from types import ModuleType from typing import TypedDict, Union, cast @@ -152,7 +152,9 @@ def estimate_stats_quality(stats: list[MetricVolumeRow]) -> StatsQualityEstimati return StatsQualityEstimation.NO_INDEXED_DATA -def get_stats_generator(use_discover: bool, remove_on_demand: bool): +def get_stats_generator( + use_discover: bool, remove_on_demand: bool +) -> Callable[[Sequence[str], str, SnubaParams, int, bool, timedelta | None], SnubaTSResult]: """ Returns a get_stats function that can fetch from either metrics or discover and with or without on_demand metrics. @@ -164,7 +166,7 @@ def get_discover_stats( snuba_params: SnubaParams, rollup: int, zerofill_results: bool, # not used but required by get_event_stats_data - comparison_delta: datetime | None, # not used but required by get_event_stats_data + comparison_delta: timedelta | None, # not used but required by get_event_stats_data ) -> SnubaTSResult: # use discover or metrics_performance depending on the dataset if use_discover: diff --git a/src/sentry/api/endpoints/relay/project_configs.py b/src/sentry/api/endpoints/relay/project_configs.py index fda297e0228bb5..b1a92480dbcd2f 100644 --- a/src/sentry/api/endpoints/relay/project_configs.py +++ b/src/sentry/api/endpoints/relay/project_configs.py @@ -41,7 +41,7 @@ class RelayProjectConfigsEndpoint(Endpoint): def post(self, request: Request): relay = request.relay assert relay is not None # should be provided during Authentication - response = {} + response: dict[str, Any] = {} if not relay.is_internal: return Response("Relay unauthorized for config information", status=403) diff --git a/src/sentry/api/endpoints/release_thresholds/release_threshold.py b/src/sentry/api/endpoints/release_thresholds/release_threshold.py index 0b72c00cbfc0a0..8f514e7ef34111 100644 --- a/src/sentry/api/endpoints/release_thresholds/release_threshold.py +++ b/src/sentry/api/endpoints/release_thresholds/release_threshold.py @@ -1,3 +1,5 @@ +from typing import TypedDict + from django.http import HttpResponse from rest_framework import serializers from rest_framework.request import Request @@ -19,19 +21,27 @@ from sentry.models.release_threshold.release_threshold import ReleaseThreshold -class ReleaseThresholdPOSTSerializer(serializers.Serializer): +class ReleaseThresholdPOSTData(TypedDict, total=False): + threshold_type: int + trigger_type: int + value: int + window_in_seconds: int + environment: object + + +class ReleaseThresholdPOSTSerializer(serializers.Serializer[ReleaseThresholdPOSTData]): threshold_type = serializers.ChoiceField(choices=ReleaseThresholdType.as_str_choices()) trigger_type = serializers.ChoiceField(choices=ReleaseThresholdTriggerType.as_str_choices()) value = serializers.IntegerField(required=True, min_value=0) window_in_seconds = serializers.IntegerField(required=True, min_value=0) environment = EnvironmentField(required=False, allow_null=True) - def validate_threshold_type(self, threshold_type: str): + def validate_threshold_type(self, threshold_type: str) -> int: if threshold_type not in THRESHOLD_TYPE_STR_TO_INT: raise serializers.ValidationError("Invalid threshold type") return THRESHOLD_TYPE_STR_TO_INT[threshold_type] - def validate_trigger_type(self, trigger_type: str): + def validate_trigger_type(self, trigger_type: str) -> int: if trigger_type not in TRIGGER_TYPE_STRING_TO_INT: raise serializers.ValidationError("Invalid trigger type") return TRIGGER_TYPE_STRING_TO_INT[trigger_type] diff --git a/src/sentry/api/endpoints/release_thresholds/release_threshold_details.py b/src/sentry/api/endpoints/release_thresholds/release_threshold_details.py index 6b6c0d066370cc..326df32b2b723b 100644 --- a/src/sentry/api/endpoints/release_thresholds/release_threshold_details.py +++ b/src/sentry/api/endpoints/release_thresholds/release_threshold_details.py @@ -1,5 +1,5 @@ import logging -from typing import Any +from typing import Any, TypedDict from django.http import HttpResponse from rest_framework import serializers @@ -21,21 +21,29 @@ from sentry.models.release_threshold.constants import TriggerType as ReleaseThresholdTriggerType from sentry.models.release_threshold.release_threshold import ReleaseThreshold + +class ReleaseThresholdPUTData(TypedDict): + threshold_type: int + trigger_type: int + value: int + window_in_seconds: int + + logger = logging.getLogger("sentry.release_thresholds") -class ReleaseThresholdPUTSerializer(serializers.Serializer): +class ReleaseThresholdPUTSerializer(serializers.Serializer[ReleaseThresholdPUTData]): threshold_type = serializers.ChoiceField(choices=ReleaseThresholdType.as_str_choices()) trigger_type = serializers.ChoiceField(choices=ReleaseThresholdTriggerType.as_str_choices()) value = serializers.IntegerField(required=True, min_value=0) window_in_seconds = serializers.IntegerField(required=True, min_value=0) - def validate_threshold_type(self, threshold_type: str): + def validate_threshold_type(self, threshold_type: str) -> int: if threshold_type not in THRESHOLD_TYPE_STR_TO_INT: raise serializers.ValidationError("Invalid threshold type") return THRESHOLD_TYPE_STR_TO_INT[threshold_type] - def validate_trigger_type(self, trigger_type: str): + def validate_trigger_type(self, trigger_type: str) -> int: if trigger_type not in TRIGGER_TYPE_STRING_TO_INT: raise serializers.ValidationError("Invalid trigger type") return TRIGGER_TYPE_STRING_TO_INT[trigger_type] @@ -54,9 +62,9 @@ class ReleaseThresholdDetailsEndpoint(ProjectEndpoint): def convert_args( self, request: Request, - *args, - **kwargs, - ) -> Any: + *args: Any, + **kwargs: Any, + ) -> tuple[tuple[Any, ...], dict[str, Any]]: parsed_args, parsed_kwargs = super().convert_args(request, *args, **kwargs) try: parsed_kwargs["release_threshold"] = ReleaseThreshold.objects.get( diff --git a/src/sentry/api/endpoints/release_thresholds/release_threshold_index.py b/src/sentry/api/endpoints/release_thresholds/release_threshold_index.py index e8232642ac6f17..f83c25a5a91144 100644 --- a/src/sentry/api/endpoints/release_thresholds/release_threshold_index.py +++ b/src/sentry/api/endpoints/release_thresholds/release_threshold_index.py @@ -1,3 +1,5 @@ +from typing import TypedDict + from django.db.models import Q from django.http import HttpResponse from rest_framework import serializers @@ -14,7 +16,12 @@ from sentry.models.release_threshold.release_threshold import ReleaseThreshold -class ReleaseThresholdIndexGETValidator(serializers.Serializer): +class ReleaseThresholdIndexGETData(TypedDict, total=False): + environment: list[str] + project: list[int] + + +class ReleaseThresholdIndexGETValidator(serializers.Serializer[ReleaseThresholdIndexGETData]): environment = serializers.ListField( required=False, allow_empty=True, child=serializers.CharField() ) diff --git a/src/sentry/api/endpoints/release_thresholds/release_threshold_status_index.py b/src/sentry/api/endpoints/release_thresholds/release_threshold_status_index.py index d5ef59b1472a3d..70d8630ad9a749 100644 --- a/src/sentry/api/endpoints/release_thresholds/release_threshold_status_index.py +++ b/src/sentry/api/endpoints/release_thresholds/release_threshold_status_index.py @@ -3,7 +3,7 @@ import logging from collections import defaultdict from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, DefaultDict +from typing import TYPE_CHECKING, Any, DefaultDict, TypedDict from django.db.models import F, Q from django.http import HttpResponse @@ -37,6 +37,7 @@ from sentry.models.release import Release from sentry.models.release_threshold.constants import ReleaseThresholdType from sentry.organizations.services.organization import RpcOrganization +from sentry.release_health.base import SessionsQueryResult from sentry.utils import metrics logger = logging.getLogger("sentry.release_threshold_status") @@ -49,7 +50,17 @@ from sentry.models.releaseprojectenvironment import ReleaseProjectEnvironment -class ReleaseThresholdStatusIndexSerializer(serializers.Serializer): +class ReleaseThresholdStatusIndexData(TypedDict, total=False): + start: datetime + end: datetime + environment: list[str] + projectSlug: list[str] + release: list[str] + + +class ReleaseThresholdStatusIndexSerializer( + serializers.Serializer[ReleaseThresholdStatusIndexData] +): start = serializers.DateTimeField( help_text="The start of the time series range as an explicit datetime, either in UTC ISO8601 or epoch seconds. " "Use along with `end`.", @@ -81,7 +92,7 @@ class ReleaseThresholdStatusIndexSerializer(serializers.Serializer): help_text=("A list of release versions to filter your results by."), ) - def validate(self, data): + def validate(self, data: ReleaseThresholdStatusIndexData) -> ReleaseThresholdStatusIndexData: if data["start"] >= data["end"]: raise serializers.ValidationError("Start datetime must be after End") return data @@ -195,7 +206,7 @@ def get(self, request: Request, organization: Organization | RpcOrganization) -> # ======================================================================== # Step 3: flatten thresholds and compile projects/release-thresholds by type # ======================================================================== - thresholds_by_type: DefaultDict[int, dict[str, list]] = defaultdict() + thresholds_by_type: DefaultDict[int, dict[str, list[Any]]] = defaultdict() query_windows_by_type: DefaultDict[int, dict[str, datetime]] = defaultdict() for release in queryset: # TODO: @@ -389,7 +400,7 @@ def get(self, request: Request, organization: Organization | RpcOrganization) -> elif threshold_type == ReleaseThresholdType.CRASH_FREE_SESSION_RATE: metrics.incr("release.threshold_health_status.check.crash_free_session_rate") query_window = query_windows_by_type[threshold_type] - sessions_data = {} + sessions_data: SessionsQueryResult | None = None try: sessions_data = fetch_sessions_data( end=query_window["end"], @@ -416,7 +427,7 @@ def get(self, request: Request, organization: Organization | RpcOrganization) -> if sessions_data: for ethreshold in category_thresholds: is_healthy, rate = is_crash_free_rate_healthy_check( - ethreshold, sessions_data, CRASH_SESSIONS_DISPLAY + ethreshold, dict(sessions_data), CRASH_SESSIONS_DISPLAY ) ethreshold.update({"is_healthy": is_healthy, "metric_value": rate}) release_threshold_health[ethreshold["key"]].append(ethreshold) diff --git a/src/sentry/api/endpoints/release_thresholds/utils/fetch_sessions_data.py b/src/sentry/api/endpoints/release_thresholds/utils/fetch_sessions_data.py index 09380f7f242666..b41946136e8fc2 100644 --- a/src/sentry/api/endpoints/release_thresholds/utils/fetch_sessions_data.py +++ b/src/sentry/api/endpoints/release_thresholds/utils/fetch_sessions_data.py @@ -11,6 +11,7 @@ from sentry.api.utils import handle_query_errors from sentry.models.organization import Organization from sentry.organizations.services.organization.model import RpcOrganization +from sentry.release_health.base import SessionsQueryResult from sentry.snuba.sessions_v2 import QueryDefinition @@ -21,7 +22,7 @@ def fetch_sessions_data( end: datetime, start: datetime, field: str | None = "sum(session)", # alternatively count_unique(user) -): +) -> SessionsQueryResult: """ This implementation was derived from organization_sessions GET endpoint NOTE: Params are derived from the request query and pulls the relevant project/environment objects diff --git a/src/sentry/api/endpoints/release_thresholds/utils/get_new_issue_counts.py b/src/sentry/api/endpoints/release_thresholds/utils/get_new_issue_counts.py index 074df359aa8acd..772f0ea69661c1 100644 --- a/src/sentry/api/endpoints/release_thresholds/utils/get_new_issue_counts.py +++ b/src/sentry/api/endpoints/release_thresholds/utils/get_new_issue_counts.py @@ -17,7 +17,7 @@ def get_new_issue_counts( constructs a query for each threshold, filtering on project NOTE: group messages are guaranteed to have a related groupenvironment """ - queryset: QuerySet | None = None + queryset: QuerySet[Group, dict[str, Any]] | None = None for t in thresholds: env: dict[str, Any] = t.get("environment") or {} query = Q( diff --git a/src/sentry/api/endpoints/system_options.py b/src/sentry/api/endpoints/system_options.py index 934ddad054202b..b68189e344f8d5 100644 --- a/src/sentry/api/endpoints/system_options.py +++ b/src/sentry/api/endpoints/system_options.py @@ -84,7 +84,7 @@ def has_permission(self, request: Request) -> bool: return True - def put(self, request: Request): + def put(self, request: Request) -> Response: if not self.has_permission(request): return Response(status=403) diff --git a/src/sentry/api/helpers/default_inbound_filters.py b/src/sentry/api/helpers/default_inbound_filters.py index f867d683b1e94f..eb97f4be35a6d1 100644 --- a/src/sentry/api/helpers/default_inbound_filters.py +++ b/src/sentry/api/helpers/default_inbound_filters.py @@ -1,17 +1,21 @@ +from collections.abc import Sequence + from sentry.ingest import inbound_filters +from sentry.models.organization import Organization +from sentry.models.project import Project # Turns on certain inbound filters by default for project. def set_default_inbound_filters( - project, - organization, - filters=( + project: Project, + organization: Organization, + filters: Sequence[str] = ( "browser-extensions", "legacy-browsers", "web-crawlers", "filtered-transaction", ), -): +) -> None: browser_subfilters = [ "ie", diff --git a/src/sentry/api/helpers/default_symbol_sources.py b/src/sentry/api/helpers/default_symbol_sources.py index 55430e5e0cf6bd..a0adf06a64083a 100644 --- a/src/sentry/api/helpers/default_symbol_sources.py +++ b/src/sentry/api/helpers/default_symbol_sources.py @@ -10,7 +10,7 @@ } -def set_default_symbol_sources(project: Project | RpcProject): +def set_default_symbol_sources(project: Project | RpcProject) -> None: if project.platform and project.platform in DEFAULT_SYMBOL_SOURCES: project.update_option( "sentry:builtin_symbol_sources", DEFAULT_SYMBOL_SOURCES[project.platform] diff --git a/src/sentry/api/helpers/group_index/__init__.py b/src/sentry/api/helpers/group_index/__init__.py index 8eef633c0d40ab..cf19a6371caca8 100644 --- a/src/sentry/api/helpers/group_index/__init__.py +++ b/src/sentry/api/helpers/group_index/__init__.py @@ -1,6 +1,7 @@ from collections.abc import Callable, Mapping from typing import Any +from sentry.models.group import Group from sentry.utils.cursors import CursorResult """TODO(mgaeta): This directory is incorrectly suffixed '_index'.""" @@ -16,7 +17,7 @@ # `sentry.api.paginator.BasePaginator.get_result`. SEARCH_MAX_HITS = 1000 -SearchFunction = Callable[[Mapping[str, Any]], tuple[CursorResult, Mapping[str, Any]]] +SearchFunction = Callable[[Mapping[str, Any]], tuple[CursorResult[Group], Mapping[str, Any]]] __all__ = ( "ACTIVITIES_COUNT", diff --git a/src/sentry/api/helpers/group_index/validators/group.py b/src/sentry/api/helpers/group_index/validators/group.py index efa3a9563181b9..0be6d0ac397d84 100644 --- a/src/sentry/api/helpers/group_index/validators/group.py +++ b/src/sentry/api/helpers/group_index/validators/group.py @@ -7,7 +7,7 @@ from sentry.api.fields import ActorField from sentry.api.helpers.group_index.validators.inbox_details import InboxDetailsValidator from sentry.api.helpers.group_index.validators.status_details import StatusDetailsValidator -from sentry.models.group import STATUS_UPDATE_CHOICES +from sentry.models.group import STATUS_UPDATE_CHOICES, Group from sentry.types.actor import Actor from sentry.types.group import SUBSTATUS_UPDATE_CHOICES, PriorityLevel @@ -23,7 +23,7 @@ "snoozeDuration", ] ) -class GroupValidator(serializers.Serializer): +class GroupValidator(serializers.Serializer[Group]): inbox = serializers.BooleanField( help_text="If true, marks the issue as reviewed by the requestor." ) diff --git a/src/sentry/api/helpers/group_index/validators/in_commit.py b/src/sentry/api/helpers/group_index/validators/in_commit.py index ef0bf6d0e3923b..bdb72a31645b95 100644 --- a/src/sentry/api/helpers/group_index/validators/in_commit.py +++ b/src/sentry/api/helpers/group_index/validators/in_commit.py @@ -13,7 +13,7 @@ class InCommitResult(TypedDict): @extend_schema_serializer() -class InCommitValidator(serializers.Serializer): +class InCommitValidator(serializers.Serializer[InCommitResult]): commit = serializers.CharField(required=True, help_text="The SHA of the resolving commit.") repository = serializers.CharField( required=True, help_text="The name of the repository (as it appears in Sentry)." diff --git a/src/sentry/api/helpers/group_index/validators/inbox_details.py b/src/sentry/api/helpers/group_index/validators/inbox_details.py index 8447e2722bd2ad..bb9f28a8c7bd45 100644 --- a/src/sentry/api/helpers/group_index/validators/inbox_details.py +++ b/src/sentry/api/helpers/group_index/validators/inbox_details.py @@ -1,6 +1,8 @@ +from typing import Never + from rest_framework import serializers -class InboxDetailsValidator(serializers.Serializer): +class InboxDetailsValidator(serializers.Serializer[Never]): # Support undo / snooze reasons pass diff --git a/src/sentry/api/helpers/group_index/validators/status_details.py b/src/sentry/api/helpers/group_index/validators/status_details.py index d626efd32285fd..eaab67aa1a7dc9 100644 --- a/src/sentry/api/helpers/group_index/validators/status_details.py +++ b/src/sentry/api/helpers/group_index/validators/status_details.py @@ -21,7 +21,7 @@ class StatusDetailsResult(TypedDict): @extend_schema_serializer() -class StatusDetailsValidator(serializers.Serializer): +class StatusDetailsValidator(serializers.Serializer[StatusDetailsResult]): inNextRelease = serializers.BooleanField( help_text="If true, marks the issue as resolved in the next release." ) diff --git a/src/sentry/api/helpers/releases.py b/src/sentry/api/helpers/releases.py index 371a6f10190582..d11688c8497d18 100644 --- a/src/sentry/api/helpers/releases.py +++ b/src/sentry/api/helpers/releases.py @@ -1,11 +1,15 @@ from sentry.api.exceptions import ResourceDoesNotExist from sentry.models.grouplink import GroupLink from sentry.models.groupresolution import GroupResolution +from sentry.models.organization import Organization from sentry.models.release import Release from sentry.models.releasecommit import ReleaseCommit +from sentry.organizations.services.organization import RpcOrganization -def get_group_ids_resolved_in_release(organization, version): +def get_group_ids_resolved_in_release( + organization: Organization | RpcOrganization, version: str +) -> set[int]: try: release = Release.objects.get(version=version, organization=organization) except Release.DoesNotExist: diff --git a/src/sentry/api/helpers/slugs.py b/src/sentry/api/helpers/slugs.py index a20dd9a002db4b..e05eea523b8cc1 100644 --- a/src/sentry/api/helpers/slugs.py +++ b/src/sentry/api/helpers/slugs.py @@ -20,7 +20,7 @@ def validate_sentry_slug(slug: str) -> None: validator(slug) -def sentry_slugify(slug: str, allow_unicode=False) -> str: +def sentry_slugify(slug: str, allow_unicode: bool = False) -> str: """ Slugify a string using Django's built-in slugify function. Ensures that the slug is not entirely numeric by adding 3 letter suffix if necessary. diff --git a/src/sentry/api/helpers/teams.py b/src/sentry/api/helpers/teams.py index 582519f7d539bf..210f095f8d8c21 100644 --- a/src/sentry/api/helpers/teams.py +++ b/src/sentry/api/helpers/teams.py @@ -1,10 +1,18 @@ +from __future__ import annotations + +from collections.abc import Iterable + +from django.db.models.query import QuerySet from rest_framework.exceptions import PermissionDenied +from rest_framework.request import Request from sentry.auth.superuser import is_active_superuser from sentry.exceptions import InvalidParams +from sentry.models.organization import Organization from sentry.models.organizationmember import OrganizationMember from sentry.models.organizationmemberteam import OrganizationMemberTeam from sentry.models.team import Team, TeamStatus +from sentry.organizations.services.organization.model import RpcOrganization def is_team_admin(org_member: OrganizationMember, team: Team | None = None) -> bool: @@ -18,7 +26,11 @@ def is_team_admin(org_member: OrganizationMember, team: Team | None = None) -> b return omt.exists() -def get_teams(request, organization, teams=None): +def get_teams( + request: Request, + organization: Organization | RpcOrganization, + teams: Iterable[int | str] | None = None, +) -> QuerySet[Team]: # do normal teams lookup based on request params requested_teams = set(request.GET.getlist("team", []) if teams is None else teams) diff --git a/src/sentry/api/helpers/user_reports.py b/src/sentry/api/helpers/user_reports.py index d2620cb52926b1..2ee5fb44cf03ca 100644 --- a/src/sentry/api/helpers/user_reports.py +++ b/src/sentry/api/helpers/user_reports.py @@ -1,7 +1,10 @@ +from collections.abc import Sequence + from sentry.models.group import Group, GroupStatus +from sentry.models.userreport import UserReport -def user_reports_filter_to_unresolved(user_reports): +def user_reports_filter_to_unresolved(user_reports: Sequence[UserReport]) -> list[UserReport]: group_ids = {ur.group_id for ur in user_reports if ur.group_id} unresolved_group_ids = set() if group_ids: diff --git a/src/sentry/apidocs/examples/project_examples.py b/src/sentry/apidocs/examples/project_examples.py index d497a0ba882b5c..bfb38def8e6e2f 100644 --- a/src/sentry/apidocs/examples/project_examples.py +++ b/src/sentry/apidocs/examples/project_examples.py @@ -1,3 +1,5 @@ +from typing import Any + from drf_spectacular.utils import OpenApiExample KEY_RATE_LIMIT = { @@ -375,7 +377,7 @@ ] -def project_with_team(extra_team: bool = False): +def project_with_team(extra_team: bool = False) -> dict[str, Any]: teams = [ { "id": "2349234102", diff --git a/src/sentry/audit_log/events.py b/src/sentry/audit_log/events.py index 58a8e21bb71d8a..0ac39f9be9867f 100644 --- a/src/sentry/audit_log/events.py +++ b/src/sentry/audit_log/events.py @@ -23,10 +23,10 @@ def _get_member_display(email: str | None, target_user: User | None) -> str: class MemberAddAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=2, name="MEMBER_ADD", api_name="member.add") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if audit_log_entry.target_user == audit_log_entry.actor: return "joined the organization" @@ -35,10 +35,10 @@ def render(self, audit_log_entry: AuditLogEntry): class MemberEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=4, name="MEMBER_EDIT", api_name="member.edit") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: member = _get_member_display(audit_log_entry.data.get("email"), audit_log_entry.target_user) role = audit_log_entry.data.get("role") or "N/A" @@ -50,10 +50,10 @@ def render(self, audit_log_entry: AuditLogEntry): class MemberRemoveAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=5, name="MEMBER_REMOVE", api_name="member.remove") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if audit_log_entry.target_user == audit_log_entry.actor: return "left the organization" @@ -62,10 +62,10 @@ def render(self, audit_log_entry: AuditLogEntry): class MemberJoinTeamAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=6, name="MEMBER_JOIN_TEAM", api_name="member.join-team") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if audit_log_entry.target_user == audit_log_entry.actor: return "joined team {team_slug}".format(**audit_log_entry.data) @@ -76,10 +76,10 @@ def render(self, audit_log_entry: AuditLogEntry): class MemberLeaveTeamAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=7, name="MEMBER_LEAVE_TEAM", api_name="member.leave-team") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if audit_log_entry.target_user == audit_log_entry.actor: return "left team {team_slug}".format(**audit_log_entry.data) @@ -90,10 +90,10 @@ def render(self, audit_log_entry: AuditLogEntry): class MemberPendingAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=8, name="MEMBER_PENDING", api_name="member.pending") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: user_display_name = _get_member_display( audit_log_entry.data.get("email"), audit_log_entry.target_user ) @@ -101,39 +101,39 @@ def render(self, audit_log_entry: AuditLogEntry): class OrgAddAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=10, name="ORG_ADD", api_name="org.create") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if channel := audit_log_entry.data.get("channel"): return f"created the organization with {channel} integration" return "created the organization" class OrgEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=11, name="ORG_EDIT", api_name="org.edit") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: items_string = ", ".join(f"{k} {v}" for k, v in audit_log_entry.data.items()) return "edited the organization setting: " + items_string class TeamEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=21, name="TEAM_EDIT", api_name="team.edit") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: slug = audit_log_entry.data["slug"] return f"edited team {slug}" class ProjectEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=31, name="PROJECT_EDIT", api_name="project.edit") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if "old_slug" in audit_log_entry.data: return "renamed project slug from {old_slug} to {new_slug}".format( **audit_log_entry.data @@ -145,10 +145,10 @@ def render(self, audit_log_entry: AuditLogEntry): class ProjectKeyEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=51, name="PROJECTKEY_EDIT", api_name="projectkey.edit") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: items_strings = [] if "prev_rate_limit_count" in audit_log_entry.data: items_strings.append( @@ -171,14 +171,14 @@ def render(self, audit_log_entry: AuditLogEntry): class ProjectPerformanceDetectionSettingsAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__( event_id=178, name="PROJECT_PERFORMANCE_ISSUE_DETECTION_CHANGE", api_name="project.change-performance-issue-detection", ) - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: from sentry.issues.endpoints.project_performance_issue_settings import ( project_settings_to_group_map as map, ) @@ -209,91 +209,91 @@ def render_project_action(audit_log_entry: AuditLogEntry, action: str): class ProjectEnableAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=37, name="PROJECT_ENABLE", api_name="project.enable") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: return render_project_action(audit_log_entry, "enable") class ProjectDisableAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=38, name="PROJECT_DISABLE", api_name="project.disable") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: return render_project_action(audit_log_entry, "disable") class ProjectOwnershipRuleEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__( event_id=179, name="PROJECT_OWNERSHIPRULE_EDIT", api_name="project.ownership-rule.edit" ) - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: return "modified ownership rules" class SSOEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=62, name="SSO_EDIT", api_name="sso.edit") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: settings = ", ".join(f"{k} {v}" for k, v in audit_log_entry.data.items()) return "edited sso settings: " + settings class ServiceHookAddAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=100, name="SERVICEHOOK_ADD", api_name="servicehook.create") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: full_url = audit_log_entry.data.get("url") return f'added a service hook for "{truncatechars(full_url, 64)}"' class ServiceHookEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=101, name="SERVICEHOOK_EDIT", api_name="servicehook.edit") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: full_url = audit_log_entry.data.get("url") return f'edited the service hook for "{truncatechars(full_url, 64)}"' class ServiceHookRemoveAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=102, name="SERVICEHOOK_REMOVE", api_name="servicehook.remove") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: full_url = audit_log_entry.data.get("url") return f'removed the service hook for "{truncatechars(full_url, 64)}"' class IntegrationDisabledAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=108, name="INTEGRATION_DISABLED", api_name="integration.disable") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: provider = audit_log_entry.data.get("provider") or "" return f"disabled {provider} integration".format(**audit_log_entry.data) class IntegrationUpgradeAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=109, name="INTEGRATION_UPGRADE", api_name="integration.upgrade") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if audit_log_entry.data.get("provider"): return "upgraded {name} for the {provider} integration".format(**audit_log_entry.data) return "updated an integration" class IntegrationAddAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=110, name="INTEGRATION_ADD", api_name="integration.add") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if audit_log_entry.data.get("provider"): return "installed {name} for the {provider} integration".format(**audit_log_entry.data) return "enabled integration {integration} for project {project}".format( @@ -302,10 +302,10 @@ def render(self, audit_log_entry: AuditLogEntry): class IntegrationEditAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=111, name="INTEGRATION_EDIT", api_name="integration.edit") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if audit_log_entry.data.get("provider"): return "edited the {name} for the {provider} integration".format(**audit_log_entry.data) return "edited integration {integration} for project {project}".format( @@ -314,10 +314,10 @@ def render(self, audit_log_entry: AuditLogEntry): class IntegrationRemoveAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__(event_id=112, name="INTEGRATION_REMOVE", api_name="integration.remove") - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if audit_log_entry.data.get("provider"): return "uninstalled {name} for the {provider} integration".format( **audit_log_entry.data @@ -328,38 +328,38 @@ def render(self, audit_log_entry: AuditLogEntry): class InternalIntegrationAddAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__( event_id=130, name="INTERNAL_INTEGRATION_ADD", api_name="internal-integration.create" ) - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: integration_name = audit_log_entry.data.get("name") or "" return f"created internal integration {integration_name}" class InternalIntegrationDisabledAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__( event_id=131, name="INTERNAL_INTEGRATION_DISABLED", api_name="internal-integration.disable", ) - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: integration_name = audit_log_entry.data.get("name") or "" return f"disabled internal integration {integration_name}".format(**audit_log_entry.data) class MonitorAddAuditLogEvent(AuditLogEvent): - def __init__(self): + def __init__(self) -> None: super().__init__( event_id=120, name="MONITOR_ADD", api_name="monitor.add", ) - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: entry_data = audit_log_entry.data name = entry_data.get("name") upsert = entry_data.get("upsert") diff --git a/src/sentry/audit_log/manager.py b/src/sentry/audit_log/manager.py index 570c0aa350635d..fa2e0fac0b99f1 100644 --- a/src/sentry/audit_log/manager.py +++ b/src/sentry/audit_log/manager.py @@ -73,7 +73,7 @@ def __init__(self, event_id, name, api_name, template=None): self.api_name = api_name self.template = template - def render(self, audit_log_entry: AuditLogEntry): + def render(self, audit_log_entry: AuditLogEntry) -> str: if not self.template: return "" return self.template.format(**audit_log_entry.data) diff --git a/src/sentry/auth/authenticators/base.py b/src/sentry/auth/authenticators/base.py index 71d4ff00f39ee3..30efd2336e8afb 100644 --- a/src/sentry/auth/authenticators/base.py +++ b/src/sentry/auth/authenticators/base.py @@ -59,18 +59,18 @@ class NewEnrollmentDisallowed(Exception): class AuthenticatorInterface: - type = -1 + type: int = -1 interface_id: str name: str | _StrPromise description: str | _StrPromise rotation_warning: str | _StrPromise | None = None - is_backup_interface = False - enroll_button = _("Enroll") - configure_button = _("Info") + is_backup_interface: bool = False + enroll_button: str | _StrPromise = _("Enroll") + configure_button: str | _StrPromise = _("Info") remove_button: str | _StrPromise | None = _("Remove 2FA method") - is_available = True - allow_multi_enrollment = False - allow_rotation_in_place = False + is_available: bool = True + allow_multi_enrollment: bool = False + allow_rotation_in_place: bool = False authenticator: Authenticator | None status: EnrollmentStatus _unbound_config: dict[Any, Any] @@ -190,7 +190,9 @@ def validate_otp(self, otp: str) -> bool: """ return False - def validate_response(self, request: Request, challenge, response) -> bool: + def validate_response( + self, request: Request, challenge: bytes | None, response: dict[str, Any] + ) -> bool: """If the activation generates a challenge that needs to be responded to this validates the response for that challenge. This is only ever called for challenges emitted by the activation of this diff --git a/src/sentry/auth/authenticators/sms.py b/src/sentry/auth/authenticators/sms.py index db937fc2247c85..afcef5cd501433 100644 --- a/src/sentry/auth/authenticators/sms.py +++ b/src/sentry/auth/authenticators/sms.py @@ -2,7 +2,7 @@ import logging from hashlib import md5 -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from django.http.request import HttpRequest from django.utils.functional import classproperty @@ -21,7 +21,7 @@ class SMSRateLimitExceeded(Exception): - def __init__(self, phone_number: str, user_id: int | None, remote_ip) -> None: + def __init__(self, phone_number: str, user_id: int | None, remote_ip: str | None) -> None: super().__init__() self.phone_number = phone_number self.user_id = user_id @@ -43,10 +43,10 @@ class SmsInterface(OtpMixin): code_ttl = 45 @classproperty - def is_available(cls): + def is_available(cls) -> bool: return sms_available() - def generate_new_config(self): + def generate_new_config(self) -> dict[str, Any]: config = super().generate_new_config() config["phone_number"] = None return config @@ -55,11 +55,11 @@ def make_otp(self) -> TOTP: return TOTP(self.config["secret"], digits=6, interval=self.code_ttl, default_window=1) @property - def phone_number(self): + def phone_number(self) -> str: return self.config["phone_number"] @phone_number.setter - def phone_number(self, value): + def phone_number(self, value: str) -> None: self.config["phone_number"] = value def activate(self, request: HttpRequest) -> ActivationMessageResult: diff --git a/src/sentry/auth/authenticators/u2f.py b/src/sentry/auth/authenticators/u2f.py index ecb010b58cd24e..4208ba3e0268a0 100644 --- a/src/sentry/auth/authenticators/u2f.py +++ b/src/sentry/auth/authenticators/u2f.py @@ -3,7 +3,7 @@ from base64 import urlsafe_b64encode from functools import cached_property from time import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from urllib.parse import urlparse import orjson @@ -28,14 +28,15 @@ from .base import ActivationChallengeResult, AuthenticatorInterface if TYPE_CHECKING: + from sentry.users.models.authenticator import Authenticator from sentry.users.models.user import User -def decode_credential_id(device) -> str: +def decode_credential_id(device: dict[str, Any]) -> str: return urlsafe_b64encode(device["binding"].credential_data.credential_id).decode("ascii") -def create_credential_object(registeredKey: dict[str, str]) -> base: +def create_credential_object(registeredKey: dict[str, str]) -> base.AttestedCredentialData: return base.AttestedCredentialData.from_ctap1( websafe_decode(registeredKey["keyHandle"]), websafe_decode(registeredKey["publicKey"]), @@ -71,7 +72,9 @@ def webauthn_registration_server(self) -> Fido2Server: return Fido2Server(self.rp) def __init__( - self, authenticator=None, status: EnrollmentStatus = EnrollmentStatus.EXISTING + self, + authenticator: Authenticator | None = None, + status: EnrollmentStatus = EnrollmentStatus.EXISTING, ) -> None: super().__init__(authenticator, status) @@ -80,24 +83,24 @@ def __init__( ) @classproperty - def u2f_app_id(cls): + def u2f_app_id(cls) -> str: rv = options.get("u2f.app-id") return rv or absolute_uri(reverse("sentry-u2f-app-id")) @classproperty - def u2f_facets(cls): + def u2f_facets(cls) -> list[str]: facets = options.get("u2f.facets") if not facets: return [_get_url_prefix()] return [x.rstrip("/") for x in facets] @classproperty - def is_available(cls): + def is_available(cls) -> bool: url_prefix = _get_url_prefix() - return url_prefix and url_prefix.startswith("https://") + return bool(url_prefix) and url_prefix.startswith("https://") - def _get_kept_devices(self, key: str): - def _key_does_not_match(device): + def _get_kept_devices(self, key: str) -> list[dict[str, Any]]: + def _key_does_not_match(device: dict[str, Any]) -> bool: if isinstance(device["binding"], AuthenticatorData): return decode_credential_id(device) != key else: @@ -105,7 +108,7 @@ def _key_does_not_match(device): return [device for device in self.config.get("devices", ()) if _key_does_not_match(device)] - def generate_new_config(self): + def generate_new_config(self) -> dict[str, Any]: return {} def start_enrollment(self, user: User) -> tuple[cbor, Fido2Server]: @@ -123,7 +126,7 @@ def start_enrollment(self, user: User) -> tuple[cbor, Fido2Server]: ) return cbor.encode(registration_data), state - def get_u2f_devices(self): + def get_u2f_devices(self) -> list[AuthenticatorData | DeviceRegistration]: rv = [] for data in self.config.get("devices", ()): # XXX: The previous version of python-u2flib-server didn't store @@ -136,7 +139,7 @@ def get_u2f_devices(self): rv.append(DeviceRegistration(data["binding"])) return rv - def credentials(self): + def credentials(self) -> list[base.AttestedCredentialData]: credentials = [] # there are 2 types of registered keys from the registered devices, those with type # AuthenticatorData are those from WebAuthn registered devices that we don't have to modify @@ -159,15 +162,16 @@ def remove_u2f_device(self, key: str) -> bool: return True return False - def get_device_name(self, key: str): + def get_device_name(self, key: str) -> str | None: for device in self.config.get("devices", ()): if isinstance(device["binding"], AuthenticatorData): if decode_credential_id(device) == key: return device["name"] elif device["binding"]["keyHandle"] == key: return device["name"] + return None - def get_registered_devices(self): + def get_registered_devices(self) -> list[dict[str, Any]]: rv = [] for device in self.config.get("devices", ()): if isinstance(device["binding"], AuthenticatorData): @@ -192,7 +196,11 @@ def get_registered_devices(self): return rv def try_enroll( - self, enrollment_data: str, response_data: str, device_name=None, state=None + self, + enrollment_data: str, + response_data: str, + device_name: str | None = None, + state: dict[str, Any] | None = None, ) -> None: data = orjson.loads(response_data) client_data = ClientData(websafe_decode(data["response"]["clientDataJSON"])) @@ -211,7 +219,9 @@ def activate(self, request: HttpRequest) -> ActivationChallengeResult: request.session["webauthn_authentication_state"] = state return ActivationChallengeResult(challenge=cbor.encode(challenge["publicKey"])) - def validate_response(self, request: HttpRequest, challenge, response) -> bool: + def validate_response( + self, request: HttpRequest, challenge: bytes | None, response: dict[str, Any] + ) -> bool: try: credentials = self.credentials() self.webauthn_authentication_server.authenticate_complete( diff --git a/src/sentry/auth/elevated_mode.py b/src/sentry/auth/elevated_mode.py index 06252992351d70..188a5f5524e06d 100644 --- a/src/sentry/auth/elevated_mode.py +++ b/src/sentry/auth/elevated_mode.py @@ -1,8 +1,14 @@ from abc import ABC, abstractmethod +from datetime import datetime from enum import Enum +from typing import Any +from django.contrib.auth.models import AnonymousUser +from django.http import HttpResponse from django.http.request import HttpRequest +from sentry.users.models.user import User + class InactiveReason(str, Enum): INVALID_IP = "invalid-ip" @@ -28,7 +34,7 @@ def is_privileged_request(self) -> tuple[bool, InactiveReason]: pass @abstractmethod - def get_session_data(self, current_datetime=None): + def get_session_data(self, current_datetime: datetime | None = None) -> dict[str, Any] | None: pass @abstractmethod @@ -36,7 +42,7 @@ def _populate(self) -> None: pass @abstractmethod - def set_logged_in(self, user, current_datetime=None) -> None: + def set_logged_in(self, user: User, current_datetime: datetime | None = None) -> None: pass @abstractmethod @@ -44,7 +50,7 @@ def set_logged_out(self) -> None: pass @abstractmethod - def on_response(cls, response) -> None: + def on_response(cls, response: HttpResponse) -> None: pass @@ -58,6 +64,9 @@ def has_elevated_mode(request: HttpRequest) -> bool: from sentry.auth.staff import has_staff_option, is_active_staff from sentry.auth.superuser import is_active_superuser + if isinstance(request.user, AnonymousUser): + return False + if has_staff_option(request.user): return is_active_staff(request) diff --git a/src/sentry/auth/password_validation.py b/src/sentry/auth/password_validation.py index 7fd2fe1013ad24..0f4694220c4cde 100644 --- a/src/sentry/auth/password_validation.py +++ b/src/sentry/auth/password_validation.py @@ -1,5 +1,6 @@ import logging from hashlib import sha1 +from typing import Any import requests from django.conf import settings @@ -9,16 +10,17 @@ from django.utils.translation import ngettext from sentry import options +from sentry.users.models.user import User from sentry.utils.imports import import_string logger = logging.getLogger(__name__) -def get_default_password_validators(): +def get_default_password_validators() -> list[Any]: return get_password_validators(settings.AUTH_PASSWORD_VALIDATORS) -def get_password_validators(validator_config): +def get_password_validators(validator_config: list[dict[str, Any]]) -> list[Any]: validators = [] for validator in validator_config: try: @@ -31,7 +33,9 @@ def get_password_validators(validator_config): return validators -def validate_password(password, user=None, password_validators=None) -> None: +def validate_password( + password: str, user: User | None = None, password_validators: list[Any] | None = None +) -> None: """ Validate whether the password meets all validator requirements. @@ -50,7 +54,7 @@ def validate_password(password, user=None, password_validators=None) -> None: raise ValidationError(errors) -def password_validators_help_texts(password_validators=None): +def password_validators_help_texts(password_validators: list[Any] | None = None) -> list[str]: """ Return a list of all help texts of all configured validators. """ @@ -62,7 +66,7 @@ def password_validators_help_texts(password_validators=None): return help_texts -def _password_validators_help_text_html(password_validators=None) -> str: +def _password_validators_help_text_html(password_validators: list[Any] | None = None) -> str: """ Return an HTML string with all help texts of all configured validators in an