diff --git a/pyproject.toml b/pyproject.toml index 889852611a23e5..306c2acefba043 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -519,8 +519,10 @@ module = [ "sentry.api.helpers.source_map_helper", "sentry.buffer.*", "sentry.build.*", + "sentry.db.models.manager", "sentry.db.models.manager.base", "sentry.db.models.manager.base_query_set", + "sentry.db.models.manager.types", "sentry.db.models.query", "sentry.eventstore.reprocessing.redis", "sentry.eventtypes.error", diff --git a/src/sentry/db/mixin.py b/src/sentry/db/mixin.py index 477676653382f9..039cff0f5b5217 100644 --- a/src/sentry/db/mixin.py +++ b/src/sentry/db/mixin.py @@ -6,7 +6,7 @@ from django.db import router, transaction -from sentry.db.models.manager import M +from sentry.db.models.manager.types import M from sentry.models.options.organization_option import OrganizationOption logger = logging.getLogger("sentry.deletions") diff --git a/src/sentry/db/models/manager/__init__.py b/src/sentry/db/models/manager/__init__.py index 7efade886a4930..8ef74a9e7086e1 100644 --- a/src/sentry/db/models/manager/__init__.py +++ b/src/sentry/db/models/manager/__init__.py @@ -1,43 +1,5 @@ -from collections.abc import Callable, Mapping -from typing import Any, TypeVar +from __future__ import annotations -from django.db.models import Model -from django.utils.encoding import smart_str - -from sentry.utils.hashlib import md5_text - -__all__ = ("BaseManager", "BaseQuerySet", "OptionManager", "M", "Value", "ValidateFunction") - -M = TypeVar("M", bound=Model, covariant=True) -Value = Any -ValidateFunction = Callable[[Value], bool] - - -def __prep_value(model: Any, key: str, value: Model | int | str) -> str: - val = value - if isinstance(value, Model): - val = value.pk - return str(val) - - -def __prep_key(model: Any, key: str) -> str: - if key == "pk": - return str(model._meta.pk.name) - return key - - -def make_key(model: Any, prefix: str, kwargs: Mapping[str, Model | int | str]) -> str: - kwargs_bits = [] - for k, v in sorted(kwargs.items()): - k = __prep_key(model, k) - v = smart_str(__prep_value(model, k, v)) - kwargs_bits.append(f"{k}={v}") - kwargs_bits_str = ":".join(kwargs_bits) - - return f"{prefix}:{model.__name__}:{md5_text(kwargs_bits_str).hexdigest()}" - - -# Exporting these classes at the bottom to avoid circular dependencies. from .base import BaseManager -from .base_query_set import BaseQuerySet -from .option import OptionManager + +__all__ = ("BaseManager",) diff --git a/src/sentry/db/models/manager/base.py b/src/sentry/db/models/manager/base.py index ebdf55db393844..0f7ece6136cd89 100644 --- a/src/sentry/db/models/manager/base.py +++ b/src/sentry/db/models/manager/base.py @@ -14,9 +14,10 @@ from django.db.models.fields import Field from django.db.models.manager import BaseManager as DjangoBaseManager from django.db.models.signals import class_prepared, post_delete, post_init, post_save +from django.utils.encoding import smart_str -from sentry.db.models.manager import M, make_key from sentry.db.models.manager.base_query_set import BaseQuerySet +from sentry.db.models.manager.types import M from sentry.db.models.query import create_or_update from sentry.db.postgres.transactions import django_test_transaction_water_mark from sentry.silo.base import SiloLimit @@ -44,6 +45,30 @@ class ModelManagerTriggerCondition(IntEnum): ModelManagerTriggerAction = Callable[[type[Model]], None] +def __prep_value(model: Any, key: str, value: Model | int | str) -> str: + val = value + if isinstance(value, Model): + val = value.pk + return str(val) + + +def __prep_key(model: Any, key: str) -> str: + if key == "pk": + return str(model._meta.pk.name) + return key + + +def make_key(model: Any, prefix: str, kwargs: Mapping[str, Model | int | str]) -> str: + kwargs_bits = [] + for k, v in sorted(kwargs.items()): + k = __prep_key(model, k) + v = smart_str(__prep_value(model, k, v)) + kwargs_bits.append(f"{k}={v}") + kwargs_bits_str = ":".join(kwargs_bits) + + return f"{prefix}:{model.__name__}:{md5_text(kwargs_bits_str).hexdigest()}" + + class BaseManager(DjangoBaseManager.from_queryset(BaseQuerySet), Generic[M]): # type: ignore[misc] lookup_handlers = {"iexact": lambda x: x.upper()} use_for_related_fields = True diff --git a/src/sentry/db/models/manager/base_query_set.py b/src/sentry/db/models/manager/base_query_set.py index 5e02fd41567c7b..dcd21db6e65c87 100644 --- a/src/sentry/db/models/manager/base_query_set.py +++ b/src/sentry/db/models/manager/base_query_set.py @@ -7,7 +7,7 @@ from django.db import connections, router, transaction from django.db.models import QuerySet, sql -from sentry.db.models.manager import M +from sentry.db.models.manager.types import M from sentry.signals import post_update diff --git a/src/sentry/db/models/manager/option.py b/src/sentry/db/models/manager/option.py index 05ab09830af1cf..f0738def24aece 100644 --- a/src/sentry/db/models/manager/option.py +++ b/src/sentry/db/models/manager/option.py @@ -6,8 +6,8 @@ from django.core.signals import request_finished from django.db.models import Model -from sentry.db.models.manager import M from sentry.db.models.manager.base import BaseManager, _local_cache +from sentry.db.models.manager.types import M class OptionManager(BaseManager[M]): diff --git a/src/sentry/db/models/manager/types.py b/src/sentry/db/models/manager/types.py new file mode 100644 index 00000000000000..b5aa9b1e5bccd8 --- /dev/null +++ b/src/sentry/db/models/manager/types.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from typing import TypeVar + +from django.db.models import Model + +M = TypeVar("M", bound=Model, covariant=True) diff --git a/src/sentry/db/models/paranoia.py b/src/sentry/db/models/paranoia.py index 11e4ec20ad7281..8bac6c712a0e9f 100644 --- a/src/sentry/db/models/paranoia.py +++ b/src/sentry/db/models/paranoia.py @@ -3,11 +3,12 @@ from django.db import models from django.utils import timezone -from sentry.db.models import BaseManager, BaseModel, BaseQuerySet, sane_repr -from sentry.db.models.manager import M +from sentry.db.models import BaseManager, BaseModel, sane_repr +from sentry.db.models.manager.base_query_set import BaseQuerySet +from sentry.db.models.manager.types import M -class ParanoidQuerySet(BaseQuerySet): +class ParanoidQuerySet(BaseQuerySet[M]): """ Prevents objects from being hard-deleted. Instead, sets the ``date_deleted``, effectively soft-deleting the object. @@ -22,7 +23,7 @@ class ParanoidManager(BaseManager[M]): Only exposes objects that have NOT been soft-deleted. """ - def get_queryset(self) -> ParanoidQuerySet: + def get_queryset(self) -> ParanoidQuerySet[M]: return ParanoidQuerySet(self.model, using=self._db).filter(date_deleted__isnull=True) diff --git a/src/sentry/models/options/organization_option.py b/src/sentry/models/options/organization_option.py index 9dbb16ea31f3cd..8a1e6d9c9479f7 100644 --- a/src/sentry/models/options/organization_option.py +++ b/src/sentry/models/options/organization_option.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from typing import TYPE_CHECKING, Any, ClassVar from django.db import models @@ -8,7 +8,7 @@ from sentry.backup.scopes import RelocationScope from sentry.db.models import FlexibleForeignKey, Model, region_silo_model, sane_repr from sentry.db.models.fields.picklefield import PickledObjectField -from sentry.db.models.manager import OptionManager, ValidateFunction, Value +from sentry.db.models.manager.option import OptionManager from sentry.utils.cache import cache if TYPE_CHECKING: @@ -30,9 +30,9 @@ def get_value( self, organization: Organization, key: str, - default: Value | None = None, - validate: ValidateFunction | None = None, - ) -> Value: + default: Any | None = None, + validate: Callable[[object], bool] | None = None, + ) -> Any: result = self.get_all_values(organization) return result.get(key, default) @@ -44,14 +44,14 @@ def unset_value(self, organization: Organization, key: str) -> None: inst.delete() self.reload_cache(organization.id, "organizationoption.unset_value") - def set_value(self, organization: Organization, key: str, value: Value) -> bool: + def set_value(self, organization: Organization, key: str, value: Any) -> bool: inst, created = self.create_or_update( organization=organization, key=key, values={"value": value} ) self.reload_cache(organization.id, "organizationoption.set_value") return bool(created) or inst > 0 - def get_all_values(self, organization: Organization | int) -> Mapping[str, Value]: + def get_all_values(self, organization: Organization | int) -> Mapping[str, Any]: if isinstance(organization, models.Model): organization_id = organization.id else: @@ -67,7 +67,7 @@ def get_all_values(self, organization: Organization | int) -> Mapping[str, Value return self._option_cache.get(cache_key, {}) - def reload_cache(self, organization_id: int, update_reason: str) -> Mapping[str, Value]: + def reload_cache(self, organization_id: int, update_reason: str) -> Mapping[str, Any]: from sentry.tasks.relay import schedule_invalidate_project_config if update_reason != "organizationoption.get_all_values": diff --git a/src/sentry/models/options/project_option.py b/src/sentry/models/options/project_option.py index 5f7f83820763e7..4d14a15755fb66 100644 --- a/src/sentry/models/options/project_option.py +++ b/src/sentry/models/options/project_option.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from typing import TYPE_CHECKING, Any, ClassVar from django.db import models @@ -11,7 +11,7 @@ from sentry.backup.scopes import ImportScope, RelocationScope from sentry.db.models import FlexibleForeignKey, Model, region_silo_model, sane_repr from sentry.db.models.fields import PickledObjectField -from sentry.db.models.manager import OptionManager, ValidateFunction, Value +from sentry.db.models.manager.option import OptionManager from sentry.utils.cache import cache if TYPE_CHECKING: @@ -88,8 +88,8 @@ def get_value( self, project: int | Project, key: str, - default: Value | None = None, - validate: ValidateFunction | None = None, + default: Any | None = None, + validate: Callable[[object], bool] | None = None, ) -> Any: result = self.get_all_values(project) if key in result: @@ -105,7 +105,7 @@ def unset_value(self, project: Project, key: str) -> None: self.filter(project=project, key=key).delete() self.reload_cache(project.id, "projectoption.unset_value") - def set_value(self, project: int | Project, key: str, value: Value) -> bool: + def set_value(self, project: int | Project, key: str, value: Any) -> bool: if isinstance(project, models.Model): project_id = project.id else: @@ -118,7 +118,7 @@ def set_value(self, project: int | Project, key: str, value: Value) -> bool: return created or inst > 0 - def get_all_values(self, project: Project | int) -> Mapping[str, Value]: + def get_all_values(self, project: Project | int) -> Mapping[str, Any]: if isinstance(project, models.Model): project_id = project.id else: @@ -134,7 +134,7 @@ def get_all_values(self, project: Project | int) -> Mapping[str, Value]: return self._option_cache.get(cache_key, {}) - def reload_cache(self, project_id: int, update_reason: str) -> Mapping[str, Value]: + def reload_cache(self, project_id: int, update_reason: str) -> Mapping[str, Any]: from sentry.tasks.relay import schedule_invalidate_project_config if update_reason != "projectoption.get_all_values": diff --git a/src/sentry/models/options/user_option.py b/src/sentry/models/options/user_option.py index 56b86e40f33fc8..f43af6b0c3c142 100644 --- a/src/sentry/models/options/user_option.py +++ b/src/sentry/models/options/user_option.py @@ -12,7 +12,7 @@ from sentry.db.models import FlexibleForeignKey, Model, control_silo_model, sane_repr from sentry.db.models.fields import PickledObjectField from sentry.db.models.fields.hybrid_cloud_foreign_key import HybridCloudForeignKey -from sentry.db.models.manager import OptionManager, Value +from sentry.db.models.manager.option import OptionManager if TYPE_CHECKING: from sentry.models.organization import Organization @@ -43,8 +43,8 @@ def _make_key( # type: ignore[override] return super()._make_key(metakey) def get_value( - self, user: User | RpcUser, key: str, default: Value | None = None, **kwargs: Any - ) -> Value: + self, user: User | RpcUser, key: str, default: Any | None = None, **kwargs: Any + ) -> Any: project = kwargs.get("project") organization = kwargs.get("organization") @@ -71,7 +71,7 @@ def unset_value(self, user: User, project: Project, key: str) -> None: return self._option_cache[metakey].pop(key, None) - def set_value(self, user: User | int, key: str, value: Value, **kwargs: Any) -> None: + def set_value(self, user: User | int, key: str, value: Any, **kwargs: Any) -> None: project = kwargs.get("project") organization = kwargs.get("organization") project_id = kwargs.get("project_id", None) @@ -106,7 +106,7 @@ def get_all_values( project: Project | int | None = None, organization: Organization | int | None = None, force_reload: bool = False, - ) -> Mapping[str, Value]: + ) -> Mapping[str, Any]: if organization and project: raise NotImplementedError(option_scope_error) diff --git a/src/sentry/models/organization.py b/src/sentry/models/organization.py index 37d4e46bbd1aa1..285d71538e9f1d 100644 --- a/src/sentry/models/organization.py +++ b/src/sentry/models/organization.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Collection, Mapping, Sequence +from collections.abc import Callable, Collection, Mapping, Sequence from enum import IntEnum from typing import TYPE_CHECKING, Any, ClassVar @@ -22,7 +22,6 @@ ) from sentry.db.models import BaseManager, BoundedPositiveIntegerField, region_silo_model, sane_repr from sentry.db.models.fields.slug import SentryOrgSlugField -from sentry.db.models.manager import ValidateFunction from sentry.db.models.outboxes import ReplicatedRegionModel from sentry.db.models.utils import slugify_instance from sentry.db.postgres.transactions import in_test_hide_transaction_boundary @@ -475,7 +474,7 @@ def get_scopes(self, role: Role) -> frozenset[str]: return frozenset(scopes) def get_option( - self, key: str, default: Any | None = None, validate: ValidateFunction | None = None + self, key: str, default: Any | None = None, validate: Callable[[object], bool] | None = None ) -> Any: return self.option_manager.get_value(self, key, default, validate) diff --git a/src/sentry/models/project.py b/src/sentry/models/project.py index 49e62b182aeaf6..22910b49d1e01e 100644 --- a/src/sentry/models/project.py +++ b/src/sentry/models/project.py @@ -2,7 +2,7 @@ import logging from collections import defaultdict -from collections.abc import Collection, Iterable, Mapping +from collections.abc import Callable, Collection, Iterable, Mapping from typing import TYPE_CHECKING, Any, ClassVar from uuid import uuid1 @@ -31,7 +31,6 @@ sane_repr, ) from sentry.db.models.fields.slug import SentrySlugField -from sentry.db.models.manager import ValidateFunction from sentry.db.models.utils import slugify_instance from sentry.locks import locks from sentry.models.grouplink import GroupLink @@ -382,7 +381,7 @@ def option_manager(self) -> ProjectOptionManager: return ProjectOption.objects def get_option( - self, key: str, default: Any | None = None, validate: ValidateFunction | None = None + self, key: str, default: Any | None = None, validate: Callable[[object], bool] | None = None ) -> Any: return self.option_manager.get_value(self, key, default, validate) diff --git a/src/sentry/models/releases/util.py b/src/sentry/models/releases/util.py index b250200438775d..5c93f1435b5ea0 100644 --- a/src/sentry/models/releases/util.py +++ b/src/sentry/models/releases/util.py @@ -11,7 +11,8 @@ from sentry_relay.exceptions import RelayError from sentry_relay.processing import parse_release -from sentry.db.models import ArrayField, BaseQuerySet +from sentry.db.models import ArrayField +from sentry.db.models.manager.base_query_set import BaseQuerySet from sentry.exceptions import InvalidSearchQuery from sentry.models.releases.release_project import ReleaseProject from sentry.utils.numbers import validate_bigint diff --git a/src/sentry/search/snuba/backend.py b/src/sentry/search/snuba/backend.py index 26539b637ba40d..85185afb9b2a9f 100644 --- a/src/sentry/search/snuba/backend.py +++ b/src/sentry/search/snuba/backend.py @@ -16,7 +16,7 @@ from sentry import features, quotas from sentry.api.event_search import SearchFilter -from sentry.db.models import BaseQuerySet +from sentry.db.models.manager.base_query_set import BaseQuerySet from sentry.exceptions import InvalidSearchQuery from sentry.issues.grouptype import ErrorGroupType from sentry.models.environment import Environment diff --git a/src/sentry/services/hybrid_cloud/organization/model.py b/src/sentry/services/hybrid_cloud/organization/model.py index a69e3ae22c615b..c2d024037a5308 100644 --- a/src/sentry/services/hybrid_cloud/organization/model.py +++ b/src/sentry/services/hybrid_cloud/organization/model.py @@ -2,7 +2,7 @@ # from __future__ import annotations # in modules such as this one where hybrid cloud data models or service classes are # defined, because we want to reflect on type annotations and avoid forward references. -from collections.abc import Iterable, Mapping, Sequence +from collections.abc import Callable, Iterable, Mapping, Sequence from datetime import datetime from enum import IntEnum from typing import Any, TypedDict @@ -12,7 +12,6 @@ from pydantic import Field from sentry import roles -from sentry.db.models import ValidateFunction, Value from sentry.roles import team_roles from sentry.roles.manager import TeamRole from sentry.services.hybrid_cloud import RpcModel @@ -208,13 +207,16 @@ def __hash__(self) -> int: return hash((self.id, self.slug)) def get_option( - self, key: str, default: Value | None = None, validate: ValidateFunction | None = None - ) -> Value: + self, + key: str, + default: Any | None = None, + validate: Callable[[object], bool] | None = None, + ) -> Any: from sentry.services.hybrid_cloud.organization import organization_service return organization_service.get_option(organization_id=self.id, key=key) - def update_option(self, key: str, value: Value) -> bool: + def update_option(self, key: str, value: Any) -> bool: from sentry.services.hybrid_cloud.organization import organization_service return organization_service.update_option(organization_id=self.id, key=key, value=value) diff --git a/src/sentry/services/hybrid_cloud/project/model.py b/src/sentry/services/hybrid_cloud/project/model.py index 0dffb7ae8f3d7c..e14d087074c3b7 100644 --- a/src/sentry/services/hybrid_cloud/project/model.py +++ b/src/sentry/services/hybrid_cloud/project/model.py @@ -3,13 +3,12 @@ # in modules such as this one where hybrid cloud data models or service classes are # defined, because we want to reflect on type annotations and avoid forward references. - -from typing import TypedDict +from collections.abc import Callable +from typing import Any, TypedDict from pydantic.fields import Field from sentry.constants import ObjectStatus -from sentry.db.models import ValidateFunction, Value from sentry.services.hybrid_cloud import OptionValue, RpcModel @@ -30,8 +29,11 @@ class RpcProject(RpcModel): platform: str | None = None def get_option( - self, key: str, default: Value | None = None, validate: ValidateFunction | None = None - ) -> Value: + self, + key: str, + default: Any | None = None, + validate: Callable[[object], bool] | None = None, + ) -> Any: from sentry.services.hybrid_cloud.project import project_service keyed_result, well_known_result = project_service.get_option(project=self, key=key) @@ -41,7 +43,7 @@ def get_option( return default return well_known_result - def update_option(self, key: str, value: Value) -> bool: + def update_option(self, key: str, value: Any) -> bool: from sentry.services.hybrid_cloud.project import project_service return project_service.update_option(self, key, value) diff --git a/src/sentry/services/hybrid_cloud/user/serial.py b/src/sentry/services/hybrid_cloud/user/serial.py index 49f96be9a5d3b3..52d2b1178f48ad 100644 --- a/src/sentry/services/hybrid_cloud/user/serial.py +++ b/src/sentry/services/hybrid_cloud/user/serial.py @@ -5,7 +5,7 @@ from django.utils.functional import LazyObject -from sentry.db.models import BaseQuerySet +from sentry.db.models.manager.base_query_set import BaseQuerySet from sentry.models.avatars.user_avatar import UserAvatar from sentry.models.user import User from sentry.services.hybrid_cloud.user import (