licomp-toolkit 0.6.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ # SPDX-FileCopyrightText: 2025 Henrik Sandklef
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import logging
6
+ from license_expression import get_spdx_licensing
7
+ from licomp_toolkit.toolkit import LicompToolkit
8
+ from licomp.interface import UseCase
9
+ from licomp.interface import Provisioning
10
+
11
+ AND = "AND"
12
+ OR = "OR"
13
+
14
+ COMPATIBILITY_TYPE = 'compatibility_type'
15
+ COMPATIBILITY_OUTBOUND_LICENSE = 'outbound_license'
16
+ COMPATIBILITY_INBOUND_LICENSE = 'inbound_license'
17
+
18
+ class LicenseExpressionParser():
19
+
20
+ def __init__(self):
21
+ self.licensing = get_spdx_licensing()
22
+
23
+ self.CLOSE_PARENTHESIS = ")"
24
+ self.LICENSE_SYMBOL = "LicenseSymbol"
25
+ self.LICENSE_WITH_SYMBOL = "LicenseWithExceptionSymbol"
26
+
27
+ def parse_license_expression(self, expression):
28
+ p = self.__parse_expression(self.licensing.parse(expression).pretty().replace('\n', ' '))
29
+ return p
30
+
31
+ def __is_license_with_exception(self, expression):
32
+ return expression.strip().startswith(self.LICENSE_WITH_SYMBOL)
33
+
34
+ def __is_license(self, expression, with_exception=False):
35
+ if with_exception:
36
+ return expression.strip().startswith(self.LICENSE_SYMBOL) or expression.strip().startswith(self.LICENSE_WITH_SYMBOL)
37
+ return expression.strip().startswith(self.LICENSE_SYMBOL)
38
+
39
+ def __is_operator(self, expression):
40
+ return expression.startswith(AND) or expression.startswith(OR)
41
+
42
+ def __get_operator(self, expression):
43
+ if expression.startswith(AND):
44
+ return AND
45
+ if expression.startswith(OR):
46
+ return OR
47
+ raise Exception("BAD EXPRESSION----")
48
+
49
+ def __get_operands_string(self, expression):
50
+ # length of the operator and parenthesis
51
+ op = self.__get_operator(expression)
52
+ op_size = len(op) + 1
53
+
54
+ # nr characters until closing (operator) parenthesis
55
+ left_parens = 1
56
+ operand_size = 1
57
+ for c in expression[op_size:]:
58
+ operand_size += 1
59
+ if c == '(':
60
+ left_parens += 1
61
+ elif c == ')':
62
+ left_parens -= 1
63
+
64
+ if left_parens == 0:
65
+ break
66
+
67
+ rest = expression[op_size:operand_size + 1]
68
+ remains = expression[operand_size - 1]
69
+ return rest, remains
70
+
71
+ def is_close(self, expression):
72
+ return expression.startswith(self.CLOSE_PARENTHESIS)
73
+
74
+ def __cleanup_license(self, operand):
75
+ stripped_operand = operand.strip()
76
+
77
+ if self.__is_license_with_exception(operand):
78
+ trimmed_operand = stripped_operand.replace(f"{self.LICENSE_WITH_SYMBOL}('", '', 1)
79
+ else:
80
+ trimmed_operand = stripped_operand.replace(f"{self.LICENSE_SYMBOL}('", '', 1)
81
+ closing_paren_index = trimmed_operand.find(")")
82
+ op = trimmed_operand[:closing_paren_index - 1]
83
+ remains = trimmed_operand[closing_paren_index + 1:].strip()
84
+ if remains.startswith(","):
85
+ remains = remains[1:]
86
+ return op, remains.strip()
87
+
88
+ def __parse_expression(self, expression):
89
+ logging.debug(f'__parse_expression: {expression}')
90
+
91
+ if self.__is_operator(expression):
92
+ operator = self.__get_operator(expression)
93
+ operands = []
94
+ ops, remains = self.__get_operands_string(expression)
95
+
96
+ while ops != "":
97
+ if self.__is_license(ops.strip(), with_exception=True):
98
+ operand, rem = self.__cleanup_license(ops)
99
+ operands.append({
100
+ COMPATIBILITY_TYPE: "license",
101
+ 'license': operand,
102
+ })
103
+ ops = rem
104
+
105
+ elif self.__is_operator(ops.strip()):
106
+ operand = self.__parse_expression(ops.strip())
107
+ operands.append(operand)
108
+ ops = ""
109
+
110
+ else:
111
+ print("uh oh ... " + str(ops))
112
+ import sys
113
+ sys.exit(1)
114
+ return {
115
+ COMPATIBILITY_TYPE: "operator",
116
+ "operator": operator,
117
+ "operands": operands,
118
+ }
119
+
120
+ elif self.__is_license(expression, with_exception=True):
121
+ cleaned_up, rem = self.__cleanup_license(expression.strip())
122
+
123
+ return {
124
+ COMPATIBILITY_TYPE: "license",
125
+ 'license': cleaned_up,
126
+ }
127
+
128
+ elif self.__is_close(expression):
129
+ return ""
130
+
131
+ raise Exception("Bottom reached")
132
+
133
+ def to_string(self, parsed_license):
134
+ license_type = parsed_license['compatibility_type']
135
+ if license_type == 'license':
136
+ return parsed_license['license']
137
+
138
+ operator = parsed_license['operator']
139
+ license_expression = []
140
+ for operand in parsed_license['operands']:
141
+ license_expression.append(f' ( {self.to_string(operand)} ) ')
142
+ return str(self.licensing.parse(operator.join(license_expression)))
143
+
144
+ class LicenseExpressionChecker():
145
+
146
+ def outbound_inbound_compatibility(self, outbound, lic):
147
+ licomp = LicompToolkit()
148
+ return licomp.outbound_inbound_compatibility(outbound,
149
+ lic,
150
+ usecase="library",
151
+ provisioning="binary-distribution")
152
+
153
+ def __compatibility_status(self, compatibility):
154
+ status = compatibility['summary']['results']
155
+
156
+ rets = []
157
+ for ret in status:
158
+ if ret == 'nr_valid':
159
+ continue
160
+ rets.append(ret)
161
+
162
+ if len(rets) == 1:
163
+ return rets[0]
164
+
165
+ return "yes"
166
+
167
+ def check_compatibility(self, outbound, parsed_expression, detailed_report=False):
168
+ compat_object = {
169
+ COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE],
170
+ 'compatiblity_check': 'outbound-operator -> inbound-license',
171
+ }
172
+
173
+ if parsed_expression[COMPATIBILITY_TYPE] == 'license':
174
+ compat_object['compatiblity_check'] = 'outbound-license -> inbound-license'
175
+ lic = parsed_expression['license']
176
+ compat = self.outbound_inbound_compatibility(outbound, lic)
177
+ compat_object['compatibility'] = self.__compatibility_status(compat)
178
+ if detailed_report:
179
+ compat_object['compatibility_details'] = compat
180
+
181
+ compat_object['inbound_license'] = lic
182
+ compat_object['outbound_license'] = outbound
183
+
184
+ else:
185
+ operator = parsed_expression['operator']
186
+ operands = parsed_expression['operands']
187
+ compat_object['compatibility_object'] = {
188
+ 'operator': operator,
189
+ 'operands': [],
190
+ }
191
+
192
+ operands_object = []
193
+ for operand in operands:
194
+ operand_compat = self.check_compatibility(outbound, operand, detailed_report=detailed_report)
195
+ operand_object = {
196
+ 'compatibility_object': operand_compat,
197
+ 'compatibility': operand_compat['compatibility'],
198
+ }
199
+ operands_object.append(operand_object)
200
+
201
+ compat_object['compatibility'] = self.summarise_compatibilities(operator, operands_object)
202
+ compat_object['compatibility_object']['operands'] = operands_object
203
+
204
+ return compat_object
205
+
206
+ def __init_summary(self, operands):
207
+ summary = {
208
+ "yes": 0,
209
+ "no": 0,
210
+ "depends": 0,
211
+ "unknown": 0,
212
+ }
213
+ for operand in operands:
214
+ compat = operand['compatibility']
215
+ summary[compat] = summary[compat] + 1
216
+ return summary
217
+
218
+ def __summarise_compatibilities_and(self, operands):
219
+ nr_operands = len(operands)
220
+ summary = self.__init_summary(operands)
221
+
222
+ if summary['no'] != 0:
223
+ return 'no'
224
+
225
+ if summary['yes'] == nr_operands:
226
+ return "yes"
227
+
228
+ return "no"
229
+
230
+ def __summarise_compatibilities_or(self, operands):
231
+ summary = self.__init_summary(operands)
232
+
233
+ if summary['yes'] != 0:
234
+ return 'yes'
235
+
236
+ return "no"
237
+
238
+ def summarise_compatibilities(self, operator, operands):
239
+ return {
240
+ AND: self.__summarise_compatibilities_and,
241
+ OR: self.__summarise_compatibilities_or,
242
+ }[operator](operands)
243
+
244
+ class _OBSOLETE_ExpressionExpressionChecker():
245
+
246
+ def __init__(self):
247
+ self.le_checker = LicenseExpressionChecker()
248
+ self.le_parser = LicenseExpressionParser()
249
+
250
+ def __parsed_expression_to_name(self, parsed_expression):
251
+ return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]]
252
+
253
+ def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed_report=False):
254
+ inbound_parsed = self.le_parser.parse_license_expression(inbound)
255
+
256
+ outbound_parsed = self.le_parser.parse_license_expression(outbound)
257
+
258
+ compatibility_report = self.__check_compatibility(outbound_parsed,
259
+ inbound_parsed,
260
+ usecase,
261
+ provisioning,
262
+ detailed_report)
263
+
264
+ return {
265
+ 'inbound': inbound,
266
+ 'outbound': outbound,
267
+ 'usecase': UseCase.usecase_to_string(usecase),
268
+ 'provisioning': Provisioning.provisioning_to_string(provisioning),
269
+ 'compatibility': compatibility_report['compatibility'],
270
+ 'compatibility_type': compatibility_report['compatibility_type'],
271
+ 'compatibility_check': f'outbound-{compatibility_report["compatibility_type"]} -> inbound-{inbound_parsed["compatibility_type"]}',
272
+ 'compatibility_report': compatibility_report,
273
+ }
274
+
275
+ def __check_compatibility(self, outbound_parsed, inbound_parsed, usecase, provisioning, detailed_report=False):
276
+
277
+ outbound_type = outbound_parsed[COMPATIBILITY_TYPE]
278
+ compat_object = {
279
+ COMPATIBILITY_TYPE: outbound_type,
280
+ 'inbound_license': self.le_parser.to_string(inbound_parsed),
281
+ 'outbound_license': self.le_parser.to_string(outbound_parsed),
282
+ }
283
+
284
+ if outbound_type == 'license':
285
+ compat_object['compatiblity_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]}'
286
+ outbound_parsed_license = outbound_parsed['license']
287
+ # Check if:
288
+ # outbound license
289
+ # is compatible with
290
+ # inbound license
291
+ compat = self.le_checker.check_compatibility(outbound_parsed_license,
292
+ inbound_parsed,
293
+ detailed_report)
294
+ compat_object['compatibility'] = compat['compatibility']
295
+ compat_object['compatibility_details'] = compat
296
+
297
+ elif outbound_type == 'operator':
298
+ compat_object['compatiblity_check'] = f'outbound-operator -> inbound-{inbound_parsed["compatibility_type"]}'
299
+ operator = outbound_parsed['operator']
300
+ operands = outbound_parsed['operands']
301
+
302
+ compat_object['compatibility_object'] = {
303
+ 'operator': operator,
304
+ 'operands': [],
305
+ }
306
+
307
+ operands_object = []
308
+ for operand in operands:
309
+ # Check if:
310
+ # operand from outbound license
311
+ # is compatible with
312
+ # inbound license
313
+ operand_compat = self.__check_compatibility(operand,
314
+ inbound_parsed,
315
+ detailed_report)
316
+ operand_object = {
317
+ 'compatibility_object': operand_compat,
318
+ 'compatibility': operand_compat['compatibility'],
319
+ }
320
+ operands_object.append(operand_object)
321
+
322
+ compat_object['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands_object)
323
+ compat_object['compatibility_object']['operands'] = operands_object
324
+
325
+ return compat_object