Coverage for apis_core/utils/helpers.py: 28%
98 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-22 07:51 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-22 07:51 +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.root_object
79 except Uri.DoesNotExist:
80 Importer = get_importer_for_model(model)
81 importer = Importer(uri, model)
82 instance = importer.create_instance()
83 uri = Uri.objects.create(uri=importer.get_uri, root_object=instance)
84 return instance
85 if raise_on_fail:
86 content_type = ContentType.objects.get_for_model(model)
87 raise ImproperlyConfigured(
88 f'Could not create {content_type.name} from string "{uri}"'
89 )
90 return False
93def get_html_diff(a, b, show_a=True, show_b=True, shorten=0):
94 """
95 Create an colorized html represenation of the difference of two values a and b
96 If `show_a` is True, colorize deletions in `a`
97 If `show_b` is True, colorize insertions in `b`
98 The value of `shorten` defines if long parts of strings that contains no change should be shortened
99 """
101 def style_remove(text):
102 return f"<span class='diff-remove'>{text}</span>"
104 def style_insert(text):
105 return f"<span class='diff-insert'>{text}</span>"
107 nones = ["", None]
108 if a in nones and b in nones:
109 result = ""
110 elif a in nones:
111 result = style_insert(b) if show_b else ""
112 elif b in nones:
113 result = style_remove(a) if show_a else ""
114 else:
115 result = ""
116 a = str(a)
117 b = str(b)
118 codes = difflib.SequenceMatcher(None, a, b).get_opcodes()
119 for opcode, a_start, a_end, b_start, b_end in codes:
120 match opcode:
121 case "equal":
122 equal = a[a_start:a_end]
123 if shorten and len(equal) > shorten:
124 equal = equal[:5] + " ... " + equal[-10:]
125 result += equal
126 case "delete":
127 if show_a:
128 result += style_remove(a[a_start:a_end])
129 case "insert":
130 if show_b:
131 result += style_insert(b[b_start:b_end])
132 case "replace":
133 if show_b:
134 result += style_insert(b[b_start:b_end])
135 if show_a:
136 result += style_remove(a[a_start:a_end])
137 return result
140def construct_lookup(value: str) -> tuple[str, str]:
141 """
142 Helper method to parse input values and construct field lookups
143 (https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups)
144 Parses user input for wildcards and returns a tuple containing the
145 interpreted django lookup string and the trimmed value
146 E.g.
148 - ``example`` -> ``('__icontains', 'example')``
149 - ``*example*`` -> ``('__icontains', 'example')``
150 - ``*example`` -> ``('__iendswith', 'example')``
151 - ``example*``-> ``('__istartswith', 'example')``
152 - ``"example"`` -> ``('__iexact', 'example')``
154 :param str value: text to be parsed for ``*``
155 :return: a tuple containing the lookup type and the value without modifiers
156 """
158 if value.startswith("*") and not value.endswith("*"):
159 value = value[1:]
160 return "__iendswith", value
162 elif not value.startswith("*") and value.endswith("*"):
163 value = value[:-1]
164 return "__istartswith", value
166 elif value.startswith('"') and value.endswith('"'):
167 value = value[1:-1]
168 return "__iexact", value
170 else:
171 if value.startswith("*") and value.endswith("*"):
172 value = value[1:-1]
173 return "__icontains", value