Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

types(python): Group API Helpers #29424

Merged
merged 7 commits into from
Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ files = src/sentry/api/bases/external_actor.py,
src/sentry/api/endpoints/project_codeowners.py,
src/sentry/api/endpoints/organization_events_stats.py,
src/sentry/api/endpoints/team_issue_breakdown.py,
src/sentry/api/helpers/group_index/**/*.py,
src/sentry/api/serializers/base.py,
src/sentry/api/serializers/models/external_actor.py,
src/sentry/api/serializers/models/integration.py,
Expand Down
20 changes: 20 additions & 0 deletions src/sentry/api/helpers/group_index/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
from typing import Any, Callable, Mapping, Tuple

from sentry.utils.cursors import CursorResult

"""TODO(mgaeta): This directory is incorrectly suffixed '_index'."""

# Bulk mutations are limited to 1000 items.
# TODO(dcramer): It'd be nice to support more than this, but it's a bit too
# complicated right now.
BULK_MUTATION_LIMIT = 1000

ACTIVITIES_COUNT = 100

# XXX: The 1000 magic number for `max_hits` is an abstraction leak from
# `sentry.api.paginator.BasePaginator.get_result`.
SEARCH_MAX_HITS = 1000

SearchFunction = Callable[[Mapping[str, Any]], Tuple[CursorResult, Mapping[str, Any]]]

__all__ = (
"ACTIVITIES_COUNT",
"BULK_MUTATION_LIMIT",
"SEARCH_MAX_HITS",
"delete_group_list",
)

from .delete import * # NOQA
from .delete import delete_group_list
from .index import * # NOQA
from .update import * # NOQA
22 changes: 17 additions & 5 deletions src/sentry/api/helpers/group_index/delete.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
import logging
from collections import defaultdict
from typing import List, Sequence
from uuid import uuid4

from rest_framework.request import Request
from rest_framework.response import Response

from sentry import eventstream
from sentry.api.base import audit_logger
from sentry.models import Group, GroupHash, GroupInbox, GroupStatus
from sentry.models import Group, GroupHash, GroupInbox, GroupStatus, Project
from sentry.signals import issue_deleted
from sentry.tasks.deletion import delete_groups as delete_groups_task
from sentry.utils.audit import create_audit_entry

from . import BULK_MUTATION_LIMIT
from . import BULK_MUTATION_LIMIT, SearchFunction
from .validators import ValidationError

delete_logger = logging.getLogger("sentry.deletions.api")


def delete_group_list(request, project, group_list, delete_type):
def delete_group_list(
request: Request,
project: "Project",
group_list: List["Group"],
delete_type: str,
) -> None:
if not group_list:
return

Expand Down Expand Up @@ -78,7 +85,12 @@ def delete_group_list(request, project, group_list, delete_type):
)


def delete_groups(request, projects, organization_id, search_fn):
def delete_groups(
request: Request,
projects: Sequence["Project"],
organization_id: int,
search_fn: SearchFunction,
) -> Response:
"""
`search_fn` refers to the `search.query` method with the appropriate
project, org, environment, and search params already bound
Expand Down Expand Up @@ -114,7 +126,7 @@ def delete_groups(request, projects, organization_id, search_fn):

for project in projects:
delete_group_list(
request, project, groups_by_project_id.get(project.id), delete_type="delete"
request, project, groups_by_project_id.get(project.id, []), delete_type="delete"
)

return Response(status=204)
87 changes: 64 additions & 23 deletions src/sentry/api/helpers/group_index/index.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,47 @@
from datetime import datetime
from typing import Any, Callable, Mapping, MutableMapping, Optional, Sequence, Tuple

import sentry_sdk
from rest_framework.exceptions import ParseError
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import features, search
from sentry.api.event_search import SearchFilter
from sentry.api.issue_search import convert_query_values, parse_search_query
from sentry.api.serializers import serialize
from sentry.app import ratelimiter
from sentry.constants import DEFAULT_SORT_OPTION
from sentry.exceptions import InvalidSearchQuery
from sentry.models import Environment, Group, Release
from sentry.models import Environment, Group, Organization, Project, Release, User
from sentry.models.group import looks_like_short_id
from sentry.signals import advanced_search_feature_gated
from sentry.utils import metrics
from sentry.utils.compat import zip
from sentry.utils.cursors import Cursor, CursorResult
from sentry.utils.hashlib import md5_text

from . import SEARCH_MAX_HITS
from .validators import ValidationError

# TODO(mgaeta): It's not currently possible to type a Callable's args with kwargs.
EndpointFunction = Callable[..., Response]


# List of conditions that mark a SearchFilter as an advanced search. Format is
# (lambda SearchFilter(): <boolean condition>, '<feature_name')
advanced_search_features = [
advanced_search_features: Sequence[Tuple[Callable[[SearchFilter], Any], str]] = [
(lambda search_filter: search_filter.is_negation, "negative search"),
(lambda search_filter: search_filter.value.is_wildcard(), "wildcard search"),
]


def build_query_params_from_request(request, organization, projects, environments):
def build_query_params_from_request(
request: Request,
organization: "Organization",
projects: Sequence["Project"],
environments: Optional[Sequence["Environment"]],
) -> MutableMapping[str, Any]:
query_kwargs = {"projects": projects, "sort_by": request.GET.get("sort", DEFAULT_SORT_OPTION)}

limit = request.GET.get("limit")
Expand Down Expand Up @@ -65,7 +80,11 @@ def build_query_params_from_request(request, organization, projects, environment
return query_kwargs


def validate_search_filter_permissions(organization, search_filters, user):
def validate_search_filter_permissions(
organization: "Organization",
search_filters: Sequence[SearchFilter],
user: "User",
) -> None:
"""
Verifies that an organization is allowed to perform the query that they
submitted.
Expand All @@ -77,7 +96,7 @@ def validate_search_filter_permissions(organization, search_filters, user):
# If the organization has advanced search, then no need to perform any
# other checks since they're allowed to use all search features
if features.has("organizations:advanced-search", organization):
return
return None

for search_filter in search_filters:
for feature_condition, feature_name in advanced_search_features:
Expand All @@ -90,17 +109,22 @@ def validate_search_filter_permissions(organization, search_filters, user):
)


def get_by_short_id(organization_id, is_short_id_lookup, query):
def get_by_short_id(
organization_id: int,
is_short_id_lookup: str,
query: str,
) -> Optional["Group"]:
if is_short_id_lookup == "1" and looks_like_short_id(query):
try:
return Group.objects.by_qualified_short_id(organization_id, query)
except Group.DoesNotExist:
pass
return None


def track_slo_response(name):
def inner_func(function):
def wrapper(request, *args, **kwargs):
def track_slo_response(name: str) -> Callable[[EndpointFunction], EndpointFunction]:
def inner_func(function: EndpointFunction) -> EndpointFunction:
def wrapper(request: Request, *args: Any, **kwargs: Any) -> Response:
from sentry.utils import snuba

try:
Expand Down Expand Up @@ -137,14 +161,14 @@ def wrapper(request, *args, **kwargs):
return inner_func


def build_rate_limit_key(function, request):
def build_rate_limit_key(function: EndpointFunction, request: Request) -> str:
ip = request.META["REMOTE_ADDR"]
return f"rate_limit_endpoint:{md5_text(function.__qualname__).hexdigest()}:{ip}"


def rate_limit_endpoint(limit=1, window=1):
def inner(function):
def wrapper(self, request, *args, **kwargs):
def rate_limit_endpoint(limit: int = 1, window: int = 1) -> EndpointFunction:
def inner(function: EndpointFunction) -> EndpointFunction:
def wrapper(self: Any, request: Request, *args: Any, **kwargs: Any) -> Response:
if ratelimiter.is_limited(
build_rate_limit_key(function, request),
limit=limit,
Expand All @@ -164,7 +188,11 @@ def wrapper(self, request, *args, **kwargs):
return inner


def calculate_stats_period(stats_period, start, end):
def calculate_stats_period(
stats_period: Optional[str],
start: Optional[datetime],
end: Optional[datetime],
) -> Tuple[Optional[str], Optional[datetime], Optional[datetime]]:
if stats_period is None:
# default
stats_period = "24h"
Expand All @@ -181,14 +209,17 @@ def calculate_stats_period(stats_period, start, end):
return stats_period, stats_period_start, stats_period_end


def prep_search(cls, request, project, extra_query_kwargs=None):
def prep_search(
cls: Any,
request: Request,
project: "Project",
extra_query_kwargs: Optional[Mapping[str, Any]] = None,
) -> Tuple[CursorResult, Mapping[str, Any]]:
try:
environment = cls._get_environment_from_request(request, project.organization_id)
except Environment.DoesNotExist:
# XXX: The 1000 magic number for `max_hits` is an abstraction leak
# from `sentry.api.paginator.BasePaginator.get_result`.
result = CursorResult([], None, None, hits=0, max_hits=1000)
query_kwargs = {}
result = CursorResult([], None, None, hits=0, max_hits=SEARCH_MAX_HITS)
query_kwargs: MutableMapping[str, Any] = {}
else:
environments = [environment] if environment is not None else environment
query_kwargs = build_query_params_from_request(
Expand All @@ -203,7 +234,10 @@ def prep_search(cls, request, project, extra_query_kwargs=None):
return result, query_kwargs


def get_first_last_release(request, group):
def get_first_last_release(
request: Request,
group: "Group",
) -> Tuple[Optional[Mapping[str, Any]], Optional[Mapping[str, Any]]]:
first_release = group.get_first_release()
if first_release is not None:
last_release = group.get_last_release()
Expand All @@ -222,7 +256,7 @@ def get_first_last_release(request, group):
return first_release, last_release


def get_release_info(request, group, version):
def get_release_info(request: Request, group: "Group", version: str) -> Mapping[str, Any]:
try:
release = Release.objects.get(
projects=group.project,
Expand All @@ -231,10 +265,17 @@ def get_release_info(request, group, version):
)
except Release.DoesNotExist:
release = {"version": version}
return serialize(release, request.user)

# Explicitly typing to satisfy mypy.
release_ifo: Mapping[str, Any] = serialize(release, request.user)
return release_ifo


def get_first_last_release_info(request, group, versions):
def get_first_last_release_info(
request: Request,
group: "Group",
versions: Sequence[str],
) -> Sequence[Mapping[str, Any]]:
releases = {
release.version: release
for release in Release.objects.filter(
Expand Down
Loading