Coverage for apis_core/utils/helpers.py: 29%

85 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-06-25 10:00 +0000

1import difflib 

2 

3from django.apps import apps 

4from django.core import serializers 

5from django.core.exceptions import ImproperlyConfigured 

6from django.db import DEFAULT_DB_ALIAS, router 

7 

8from apis_core.generic.helpers import first_member_match, module_paths 

9 

10 

11def datadump_get_objects(models: list = [], *args, **kwargs): 

12 for model in models: 

13 if not model._meta.proxy and router.allow_migrate_model( 

14 DEFAULT_DB_ALIAS, model 

15 ): 

16 objects = model._default_manager 

17 queryset = objects.using(DEFAULT_DB_ALIAS).order_by(model._meta.pk.name) 

18 yield from queryset.iterator() 

19 

20 

21def datadump_get_queryset(additional_app_labels: list = []): 

22 """ 

23 This method is loosely based on the `dumpdata` admin command. 

24 It iterates throug the relevant app models and exports them using 

25 a serializer and natural foreign keys. 

26 Data exported this way can be reimported into a newly created Django APIS app 

27 """ 

28 

29 # get all APIS apps and all APIS models 

30 apis_app_labels = ["apis_relations", "apis_metainfo"] 

31 apis_app_models = [ 

32 model for model in apps.get_models() if model._meta.app_label in apis_app_labels 

33 ] 

34 

35 # create a list of app labels we want to iterate 

36 # this allows to extend the apps via the ?app_labels= parameter 

37 app_labels = set(apis_app_labels) 

38 app_labels |= set(additional_app_labels) 

39 

40 # look for models that inherit from APIS models and add their 

41 # app label to app_labels 

42 for model in apps.get_models(): 

43 if any(map(lambda x: issubclass(model, x), apis_app_models)): 

44 app_labels.add(model._meta.app_label) 

45 

46 # now go through all app labels 

47 app_list = {} 

48 for app_label in app_labels: 

49 app_config = apps.get_app_config(app_label) 

50 app_list[app_config] = None 

51 

52 models = serializers.sort_dependencies(app_list.items(), allow_cycles=True) 

53 

54 yield from datadump_get_objects(models) 

55 

56 

57def datadump_serializer(additional_app_labels: list = [], serialier_format="json"): 

58 return serializers.serialize( 

59 serialier_format, 

60 datadump_get_queryset(additional_app_labels), 

61 use_natural_foreign_keys=True, 

62 ) 

63 

64 

65def get_importer_for_model(model: object): 

66 importer_paths = module_paths(model, path="importers", suffix="Importer") 

67 if importer := first_member_match(importer_paths): 

68 return importer 

69 raise ImproperlyConfigured(f"No suitable importer found for {model}") 

70 

71 

72def get_html_diff(a, b, show_a=True, show_b=True, shorten=0): 

73 """ 

74 Create an colorized html represenation of the difference of two values a and b 

75 If `show_a` is True, colorize deletions in `a` 

76 If `show_b` is True, colorize insertions in `b` 

77 The value of `shorten` defines if long parts of strings that contains no change should be shortened 

78 """ 

79 

80 def style_remove(text): 

81 return f"<span class='diff-remove'>{text}</span>" 

82 

83 def style_insert(text): 

84 return f"<span class='diff-insert'>{text}</span>" 

85 

86 nones = ["", None] 

87 if a in nones and b in nones: 

88 result = "" 

89 elif a in nones: 

90 result = style_insert(b) if show_b else "" 

91 elif b in nones: 

92 result = style_remove(a) if show_a else "" 

93 else: 

94 result = "" 

95 a = str(a) 

96 b = str(b) 

97 codes = difflib.SequenceMatcher(None, a, b).get_opcodes() 

98 for opcode, a_start, a_end, b_start, b_end in codes: 

99 match opcode: 

100 case "equal": 

101 equal = a[a_start:a_end] 

102 if shorten and len(equal) > shorten: 

103 equal = equal[:5] + " ... " + equal[-10:] 

104 result += equal 

105 case "delete": 

106 if show_a: 

107 result += style_remove(a[a_start:a_end]) 

108 case "insert": 

109 if show_b: 

110 result += style_insert(b[b_start:b_end]) 

111 case "replace": 

112 if show_b: 

113 result += style_insert(b[b_start:b_end]) 

114 if show_a: 

115 result += style_remove(a[a_start:a_end]) 

116 return result 

117 

118 

119def construct_lookup(value: str) -> tuple[str, str]: 

120 """ 

121 Helper method to parse input values and construct field lookups 

122 (https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups) 

123 Parses user input for wildcards and returns a tuple containing the 

124 interpreted django lookup string and the trimmed value 

125 E.g. 

126 

127 - ``example`` -> ``('__icontains', 'example')`` 

128 - ``*example*`` -> ``('__icontains', 'example')`` 

129 - ``*example`` -> ``('__iendswith', 'example')`` 

130 - ``example*``-> ``('__istartswith', 'example')`` 

131 - ``"example"`` -> ``('__iexact', 'example')`` 

132 

133 :param str value: text to be parsed for ``*`` 

134 :return: a tuple containing the lookup type and the value without modifiers 

135 """ 

136 

137 if value.startswith("*") and not value.endswith("*"): 

138 value = value[1:] 

139 return "__iendswith", value 

140 

141 elif not value.startswith("*") and value.endswith("*"): 

142 value = value[:-1] 

143 return "__istartswith", value 

144 

145 elif value.startswith('"') and value.endswith('"'): 

146 value = value[1:-1] 

147 return "__iexact", value 

148 

149 else: 

150 if value.startswith("*") and value.endswith("*"): 

151 value = value[1:-1] 

152 return "__icontains", value 

153 

154 

155def flatten_if_single(value: list): 

156 if len(value) == 1: 

157 return value[0] 

158 return value