Coverage for apis_core/utils/helpers.py: 27%
103 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-19 16:54 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-02-19 16:54 +0000
1import difflib
3from django.apps import apps
4from django.contrib.contenttypes.models import ContentType
5from django.core import serializers
6from django.core.exceptions import ImproperlyConfigured
7from django.db import DEFAULT_DB_ALIAS, router
9from apis_core.apis_metainfo.models import Uri
10from apis_core.generic.helpers import first_member_match, module_paths
13def datadump_get_objects(models: list = [], *args, **kwargs):
14 for model in models:
15 if not model._meta.proxy and router.allow_migrate_model(
16 DEFAULT_DB_ALIAS, model
17 ):
18 objects = model._default_manager
19 queryset = objects.using(DEFAULT_DB_ALIAS).order_by(model._meta.pk.name)
20 yield from queryset.iterator()
23def datadump_get_queryset(additional_app_labels: list = []):
24 """
25 This method is loosely based on the `dumpdata` admin command.
26 It iterates throug the relevant app models and exports them using
27 a serializer and natural foreign keys.
28 Data exported this way can be reimported into a newly created Django APIS app
29 """
31 # get all APIS apps and all APIS models
32 apis_app_labels = ["apis_relations", "apis_metainfo"]
33 apis_app_models = [
34 model for model in apps.get_models() if model._meta.app_label in apis_app_labels
35 ]
37 # create a list of app labels we want to iterate
38 # this allows to extend the apps via the ?app_labels= parameter
39 app_labels = set(apis_app_labels)
40 app_labels |= set(additional_app_labels)
42 # look for models that inherit from APIS models and add their
43 # app label to app_labels
44 for model in apps.get_models():
45 if any(map(lambda x: issubclass(model, x), apis_app_models)):
46 app_labels.add(model._meta.app_label)
48 # now go through all app labels
49 app_list = {}
50 for app_label in app_labels:
51 app_config = apps.get_app_config(app_label)
52 app_list[app_config] = None
54 models = serializers.sort_dependencies(app_list.items(), allow_cycles=True)
56 yield from datadump_get_objects(models)
59def datadump_serializer(additional_app_labels: list = [], serialier_format="json"):
60 return serializers.serialize(
61 serialier_format,
62 datadump_get_queryset(additional_app_labels),
63 use_natural_foreign_keys=True,
64 )
67def get_importer_for_model(model: object):
68 importer_paths = module_paths(model, path="importers", suffix="Importer")
69 if importer := first_member_match(importer_paths):
70 return importer
71 raise ImproperlyConfigured(f"No suitable importer found for {model}")
74def create_object_from_uri(uri: str, model: object, raise_on_fail=False) -> object:
75 if uri.startswith("http"):
76 try:
77 uri = Uri.objects.get(uri=uri)
78 return uri.content_object
79 except Uri.DoesNotExist:
80 Importer = get_importer_for_model(model)
81 importer = Importer(uri, model)
82 instance = importer.create_instance()
83 content_type = ContentType.objects.get_for_model(instance)
84 uri = Uri.objects.create(
85 uri=importer.get_uri,
86 content_type=content_type,
87 object_id=instance.id,
88 )
89 return instance
90 if raise_on_fail:
91 content_type = ContentType.objects.get_for_model(model)
92 raise ImproperlyConfigured(
93 f'Could not create {content_type.name} from string "{uri}"'
94 )
95 return False
98def get_html_diff(a, b, show_a=True, show_b=True, shorten=0):
99 """
100 Create an colorized html represenation of the difference of two values a and b
101 If `show_a` is True, colorize deletions in `a`
102 If `show_b` is True, colorize insertions in `b`
103 The value of `shorten` defines if long parts of strings that contains no change should be shortened
104 """
106 def style_remove(text):
107 return f"<span class='diff-remove'>{text}</span>"
109 def style_insert(text):
110 return f"<span class='diff-insert'>{text}</span>"
112 nones = ["", None]
113 if a in nones and b in nones:
114 result = ""
115 elif a in nones:
116 result = style_insert(b) if show_b else ""
117 elif b in nones:
118 result = style_remove(a) if show_a else ""
119 else:
120 result = ""
121 a = str(a)
122 b = str(b)
123 codes = difflib.SequenceMatcher(None, a, b).get_opcodes()
124 for opcode, a_start, a_end, b_start, b_end in codes:
125 match opcode:
126 case "equal":
127 equal = a[a_start:a_end]
128 if shorten and len(equal) > shorten:
129 equal = equal[:5] + " ... " + equal[-10:]
130 result += equal
131 case "delete":
132 if show_a:
133 result += style_remove(a[a_start:a_end])
134 case "insert":
135 if show_b:
136 result += style_insert(b[b_start:b_end])
137 case "replace":
138 if show_b:
139 result += style_insert(b[b_start:b_end])
140 if show_a:
141 result += style_remove(a[a_start:a_end])
142 return result
145def construct_lookup(value: str) -> tuple[str, str]:
146 """
147 Helper method to parse input values and construct field lookups
148 (https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups)
149 Parses user input for wildcards and returns a tuple containing the
150 interpreted django lookup string and the trimmed value
151 E.g.
153 - ``example`` -> ``('__icontains', 'example')``
154 - ``*example*`` -> ``('__icontains', 'example')``
155 - ``*example`` -> ``('__iendswith', 'example')``
156 - ``example*``-> ``('__istartswith', 'example')``
157 - ``"example"`` -> ``('__iexact', 'example')``
159 :param str value: text to be parsed for ``*``
160 :return: a tuple containing the lookup type and the value without modifiers
161 """
163 if value.startswith("*") and not value.endswith("*"):
164 value = value[1:]
165 return "__iendswith", value
167 elif not value.startswith("*") and value.endswith("*"):
168 value = value[:-1]
169 return "__istartswith", value
171 elif value.startswith('"') and value.endswith('"'):
172 value = value[1:-1]
173 return "__iexact", value
175 else:
176 if value.startswith("*") and value.endswith("*"):
177 value = value[1:-1]
178 return "__icontains", value
181def flatten_if_single(value: list):
182 if len(value) == 1:
183 return value[0]
184 return value