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.
- licomp_toolkit/__init__.py +0 -0
- licomp_toolkit/__main__.py +322 -0
- licomp_toolkit/config.py +40 -0
- licomp_toolkit/data/reply_schema.json +305 -0
- licomp_toolkit/display_compatibility.py +21 -0
- licomp_toolkit/exception.py +11 -0
- licomp_toolkit/expr_parser.py +139 -0
- licomp_toolkit/format.py +362 -0
- licomp_toolkit/lic_expr.py +325 -0
- licomp_toolkit/license_policy.py +564 -0
- licomp_toolkit/return_codes.py +12 -0
- licomp_toolkit/schema_checker.py +67 -0
- licomp_toolkit/suggester.py +162 -0
- licomp_toolkit/toolkit.py +504 -0
- licomp_toolkit/utils.py +81 -0
- licomp_toolkit-0.6.3.dist-info/METADATA +164 -0
- licomp_toolkit-0.6.3.dist-info/RECORD +21 -0
- licomp_toolkit-0.6.3.dist-info/WHEEL +5 -0
- licomp_toolkit-0.6.3.dist-info/entry_points.txt +2 -0
- licomp_toolkit-0.6.3.dist-info/licenses/LICENSES/GPL-3.0-or-later.txt +232 -0
- licomp_toolkit-0.6.3.dist-info/top_level.txt +1 -0
|
@@ -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
|