Browse Source

ref(backup): Split dependency function into own file (#54323)

We will re-use this functionality in multiple places, so its best to
move it now.

Issue: getsentry/team-ospo#171
Alex Zaslavsky 1 year ago
parent
commit
6612cd86e7
2 changed files with 103 additions and 99 deletions
  1. 102 0
      src/sentry/backup/dependencies.py
  2. 1 99
      src/sentry/backup/exports.py

+ 102 - 0
src/sentry/backup/dependencies.py

@@ -0,0 +1,102 @@
+from __future__ import annotations
+
+from django.db.models.fields.related import ManyToManyField
+
+from sentry.backup.helpers import EXCLUDED_APPS
+
+
+def sort_dependencies():
+    """
+    Similar to Django's except that we discard the important of natural keys
+    when sorting dependencies (i.e. it works without them).
+    """
+    from django.apps import apps
+
+    from sentry.models.actor import Actor
+    from sentry.models.team import Team
+    from sentry.models.user import User
+
+    # Process the list of models, and get the list of dependencies
+    model_dependencies = []
+    models = set()
+    for app_config in apps.get_app_configs():
+        if app_config.label in EXCLUDED_APPS:
+            continue
+
+        model_iterator = app_config.get_models()
+
+        for model in model_iterator:
+            models.add(model)
+            # Add any explicitly defined dependencies
+            if hasattr(model, "natural_key"):
+                deps = getattr(model.natural_key, "dependencies", [])
+                if deps:
+                    deps = [apps.get_model(*d.split(".")) for d in deps]
+            else:
+                deps = []
+
+            # Now add a dependency for any FK relation with a model that
+            # defines a natural key
+            for field in model._meta.fields:
+                rel_model = getattr(field.remote_field, "model", None)
+                if rel_model is not None and rel_model != model:
+                    # TODO(hybrid-cloud): actor refactor.
+                    # Add cludgy conditional preventing walking actor.team_id, actor.user_id
+                    # Which avoids circular imports
+                    if model == Actor and (rel_model == Team or rel_model == User):
+                        continue
+
+                    deps.append(rel_model)
+
+            # Also add a dependency for any simple M2M relation with a model
+            # that defines a natural key.  M2M relations with explicit through
+            # models don't count as dependencies.
+            many_to_many_fields = [
+                field for field in model._meta.get_fields() if isinstance(field, ManyToManyField)
+            ]
+            for field in many_to_many_fields:
+                rel_model = getattr(field.remote_field, "model", None)
+                if rel_model is not None and rel_model != model:
+                    deps.append(rel_model)
+
+            model_dependencies.append((model, deps))
+
+    model_dependencies.reverse()
+    # Now sort the models to ensure that dependencies are met. This
+    # is done by repeatedly iterating over the input list of models.
+    # If all the dependencies of a given model are in the final list,
+    # that model is promoted to the end of the final list. This process
+    # continues until the input list is empty, or we do a full iteration
+    # over the input models without promoting a model to the final list.
+    # If we do a full iteration without a promotion, that means there are
+    # circular dependencies in the list.
+    model_list = []
+    while model_dependencies:
+        skipped = []
+        changed = False
+        while model_dependencies:
+            model, deps = model_dependencies.pop()
+
+            # If all of the models in the dependency list are either already
+            # on the final model list, or not on the original serialization list,
+            # then we've found another model with all it's dependencies satisfied.
+            found = True
+            for candidate in ((d not in models or d in model_list) for d in deps):
+                if not candidate:
+                    found = False
+            if found:
+                model_list.append(model)
+                changed = True
+            else:
+                skipped.append((model, deps))
+        if not changed:
+            raise RuntimeError(
+                "Can't resolve dependencies for %s in serialized app list."
+                % ", ".join(
+                    f"{model._meta.app_label}.{model._meta.object_name}"
+                    for model, deps in sorted(skipped, key=lambda obj: obj[0].__name__)
+                )
+            )
+        model_dependencies = skipped
+
+    return model_list

+ 1 - 99
src/sentry/backup/exports.py

@@ -6,9 +6,8 @@ from typing import NamedTuple
 import click
 from django.core.serializers import serialize
 from django.core.serializers.json import DjangoJSONEncoder
-from django.db.models.fields.related import ManyToManyField
 
-from sentry.backup.helpers import EXCLUDED_APPS
+from sentry.backup.dependencies import sort_dependencies
 
 UTC_0 = timezone(timedelta(hours=0))
 
@@ -24,103 +23,6 @@ class DatetimeSafeDjangoJSONEncoder(DjangoJSONEncoder):
         return super().default(obj)
 
 
-def sort_dependencies():
-    """
-    Similar to Django's except that we discard the important of natural keys
-    when sorting dependencies (i.e. it works without them).
-    """
-    from django.apps import apps
-
-    from sentry.models.actor import Actor
-    from sentry.models.team import Team
-    from sentry.models.user import User
-
-    # Process the list of models, and get the list of dependencies
-    model_dependencies = []
-    models = set()
-    for app_config in apps.get_app_configs():
-        if app_config.label in EXCLUDED_APPS:
-            continue
-
-        model_iterator = app_config.get_models()
-
-        for model in model_iterator:
-            models.add(model)
-            # Add any explicitly defined dependencies
-            if hasattr(model, "natural_key"):
-                deps = getattr(model.natural_key, "dependencies", [])
-                if deps:
-                    deps = [apps.get_model(*d.split(".")) for d in deps]
-            else:
-                deps = []
-
-            # Now add a dependency for any FK relation with a model that
-            # defines a natural key
-            for field in model._meta.fields:
-                rel_model = getattr(field.remote_field, "model", None)
-                if rel_model is not None and rel_model != model:
-                    # TODO(hybrid-cloud): actor refactor.
-                    # Add cludgy conditional preventing walking actor.team_id, actor.user_id
-                    # Which avoids circular imports
-                    if model == Actor and (rel_model == Team or rel_model == User):
-                        continue
-
-                    deps.append(rel_model)
-
-            # Also add a dependency for any simple M2M relation with a model
-            # that defines a natural key.  M2M relations with explicit through
-            # models don't count as dependencies.
-            many_to_many_fields = [
-                field for field in model._meta.get_fields() if isinstance(field, ManyToManyField)
-            ]
-            for field in many_to_many_fields:
-                rel_model = getattr(field.remote_field, "model", None)
-                if rel_model is not None and rel_model != model:
-                    deps.append(rel_model)
-
-            model_dependencies.append((model, deps))
-
-    model_dependencies.reverse()
-    # Now sort the models to ensure that dependencies are met. This
-    # is done by repeatedly iterating over the input list of models.
-    # If all the dependencies of a given model are in the final list,
-    # that model is promoted to the end of the final list. This process
-    # continues until the input list is empty, or we do a full iteration
-    # over the input models without promoting a model to the final list.
-    # If we do a full iteration without a promotion, that means there are
-    # circular dependencies in the list.
-    model_list = []
-    while model_dependencies:
-        skipped = []
-        changed = False
-        while model_dependencies:
-            model, deps = model_dependencies.pop()
-
-            # If all of the models in the dependency list are either already
-            # on the final model list, or not on the original serialization list,
-            # then we've found another model with all it's dependencies satisfied.
-            found = True
-            for candidate in ((d not in models or d in model_list) for d in deps):
-                if not candidate:
-                    found = False
-            if found:
-                model_list.append(model)
-                changed = True
-            else:
-                skipped.append((model, deps))
-        if not changed:
-            raise RuntimeError(
-                "Can't resolve dependencies for %s in serialized app list."
-                % ", ".join(
-                    f"{model._meta.app_label}.{model._meta.object_name}"
-                    for model, deps in sorted(skipped, key=lambda obj: obj[0].__name__)
-                )
-            )
-        model_dependencies = skipped
-
-    return model_list
-
-
 class OldExportConfig(NamedTuple):
     """While we are migrating to the new backup system, we need to take care not to break the old
     and relatively untested workflows. This model allows us to stub in the old configs."""