licomp-toolkit 0.6.2__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,162 @@
1
+ # SPDX-FileCopyrightText: 2025 Henrik Sandklef
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import logging
6
+ import re
7
+ import traceback
8
+
9
+ from licomp_toolkit.toolkit import LicompToolkit
10
+ from licomp_toolkit.toolkit import ExpressionExpressionChecker
11
+ from licomp.interface import Provisioning
12
+ from licomp.interface import UseCase
13
+
14
+ from flame.license_db import FossLicenses
15
+
16
+ class OutboundSuggester:
17
+ """
18
+ OutboundSuggester suggests outbound candidate licenses from a
19
+ license expression
20
+
21
+ If you have a license expression like this "BSD-3-Clause AND MIT
22
+ AND GPL-2.0-only", typically the result of your dependencies'
23
+ licenses, and want to know which outbound license you can chose.
24
+
25
+ """
26
+ def __init__(self):
27
+ self._compatibility_rankings = {}
28
+ self.lt = LicompToolkit()
29
+ self.le = ExpressionExpressionChecker()
30
+ self.flame = FossLicenses()
31
+
32
+ def compatibility_rankings(self, usecase, provisioning):
33
+ """ Returns a dict with ranked licenses based on your usecase/provisioning, e.g.
34
+
35
+ {
36
+ 'GPL3': {'count': 2, 'valids': '1', 'index': 69},
37
+ 'MIT': {'count': 108, 'valids': '1', 'index': 169}
38
+ }
39
+
40
+ count - how many other licenses the license is compatible with (primary sort)
41
+ valids - how many valid resources provided a 'yes' answer (secondary sort)
42
+ index - index for the licenses, the higher index the more compatible (with other licenses)
43
+ """
44
+ if usecase in self._compatibility_rankings:
45
+ if provisioning in self._compatibility_rankings[usecase]:
46
+ return self._compatibility_rankings[usecase][provisioning]
47
+ else:
48
+ self._compatibility_rankings[usecase] = {}
49
+
50
+ #
51
+ # identify compatiblities
52
+ lic_map = {}
53
+ # loop through every license (against themselves)
54
+ for in_lic in self.lt.supported_licenses():
55
+ for out_lic in self.lt.supported_licenses():
56
+ # get compat
57
+ ret = self.lt.outbound_inbound_compatibility(out_lic,
58
+ in_lic,
59
+ UseCase.string_to_usecase(usecase),
60
+ Provisioning.string_to_provisioning(provisioning))
61
+ results = ret['summary']['results']
62
+ valid_results = results['nr_valid']
63
+ # only increase the 'count' if the answers from the resources are only 'yes'
64
+ yes = results.get('yes', {'count': 0, 'percent': 0.0})
65
+ inc = 0
66
+ if yes['percent'] == 100:
67
+ inc = 1
68
+
69
+ lic_map[in_lic] = {
70
+ 'count': lic_map.get(in_lic, {'count': 0})['count'] + inc,
71
+ 'valids': valid_results,
72
+ }
73
+
74
+ # decorate (to sort list)
75
+ decorated = [(lic_map[lic]['count'], lic_map[lic]['valids'], i, lic) for i, lic in enumerate(lic_map)]
76
+ # sort after count, valids (in tuple)
77
+ decorated.sort()
78
+ # undecorate
79
+ ordered_lic = [lic for count, valids, i, lic in decorated]
80
+
81
+ # create new dict - use license name as key
82
+ ordered_lic_map = {}
83
+ for i, lic in enumerate(ordered_lic):
84
+ ordered_lic_map[lic] = lic_map[lic]
85
+ ordered_lic_map[lic]['index'] = i
86
+
87
+ # store foar later (re)use
88
+ self._compatibility_rankings[usecase][provisioning] = ordered_lic_map
89
+
90
+ return self._compatibility_rankings[usecase][provisioning]
91
+
92
+ def licenses(self, license_expr):
93
+ """Returns a list of licenses in a license expression.
94
+
95
+ If you have a license expression like this:
96
+ "(MIT OR Apache-2.0) AND MIT AND GPL-2.0-only WITH Classpath-exception-2.0"
97
+
98
+ you get a list like this:
99
+ ["MIT", "Apache-2.0", "GPL-2.0-only WITH Classpath-exception-2.0"]
100
+
101
+ """
102
+ normalized = self.flame.expression_license(license_expr, update_dual=False)['identified_license']
103
+
104
+ with_fixed = normalized.replace(' WITH ', '-WITH-')
105
+
106
+ splits = [x.replace('-WITH-', ' WITH ') for x in re.split(r' AND | OR |\(|\)', with_fixed) if len(x) > 1]
107
+
108
+ return splits
109
+
110
+ def compat_licenses(self, license_expr, usecase, provisioning, licenses_to_check=None, resources=None):
111
+ """Returns a list of licenses that are compatible with a license expression
112
+
113
+ If you have a license expression like this:
114
+ "(MIT OR Apache-2.0) AND MIT AND GPL-2.0-only"
115
+
116
+ you get a list like this of licenses that are compatible with
117
+ the expression listed in order of ranked compatibility. If you
118
+ don't provide any licenses to check the functions defaults to
119
+ looking at the licenses in the expressions itself. This you will get a list like this:
120
+
121
+ ["GPL-2.0-only"]
122
+
123
+ """
124
+ compats = []
125
+ if not licenses_to_check:
126
+ licenses_to_check = self.licenses(license_expr)
127
+
128
+ license_expression_parsed = self.flame.expression_license(license_expr, update_dual=False)['identified_license']
129
+ for lic in licenses_to_check:
130
+ logging.debug(f' check licenses: {lic}')
131
+ try:
132
+ ret = self.le.check_compatibility(lic,
133
+ license_expression_parsed,
134
+ usecase,
135
+ provisioning,
136
+ resources)
137
+ if ret['compatibility'] == 'yes':
138
+ compats.append(lic)
139
+ logging.debug(f' appending: {lic}')
140
+ except Exception as e:
141
+ logging.debug(f'Exception caught: {e}')
142
+ logging.debug(traceback.format_exc())
143
+
144
+ # decorate compat list with indices (from rankings)
145
+ decorated = [(self.__compat_index(lic, usecase, provisioning), lic) for lic in compats]
146
+
147
+ # sort and reverse (to get the most compatible first)
148
+ decorated.sort()
149
+ decorated.reverse()
150
+
151
+ # undecorate
152
+ sorted_compats = [lic for index, lic in decorated]
153
+
154
+ return sorted_compats
155
+
156
+ def __compat_index(self, lic, usecase, provisioning):
157
+ # returns the ranking index for a license,
158
+ # given usecase and provisioning
159
+
160
+ # initialize the ranking
161
+ self.compatibility_rankings(usecase, provisioning)
162
+ return self._compatibility_rankings[usecase][provisioning][lic]['index']
@@ -0,0 +1,505 @@
1
+ # SPDX-FileCopyrightText: 2024 Henrik Sandklef
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import logging
6
+ from license_expression import Licensing
7
+
8
+ from licomp.interface import Licomp
9
+ from licomp.interface import UseCase
10
+ from licomp.interface import Provisioning
11
+ from licomp.interface import LicompException
12
+ from licomp.return_codes import ReturnCodes
13
+ from licomp.interface import CompatibilityStatus
14
+
15
+ from licomp_osadl.osadl import LicompOsadl
16
+ from licomp_reclicense.reclicense import LicompReclicense
17
+ from licomp_proprietary.proprietary import LicompProprietary
18
+ from licomp_dwheeler.dwheeler import LicompDw
19
+ from licomp_hermione.hermione import LicompHermione
20
+ from licomp_gnuguide.gnuguide import GnuQuickGuideLicense
21
+ from licomp_oslc_handbook.oslc_handbook import LicompOslcHandbook
22
+ from licomp_doubleopen.doubleopen import LicompDoubleOpen
23
+ from licomp_aflc.aflc import LicompAflc
24
+
25
+ from licomp_toolkit.config import disclaimer
26
+ from licomp_toolkit.config import licomp_toolkit_version
27
+ from licomp_toolkit.config import licomp_toolkit_file_version
28
+ from licomp_toolkit.config import cli_name
29
+ from licomp_toolkit.config import module_name
30
+
31
+ from licomp_toolkit.expr_parser import LicenseExpressionParser
32
+ from licomp_toolkit.expr_parser import COMPATIBILITY_TYPE
33
+ from licomp_toolkit.expr_parser import AND
34
+ from licomp_toolkit.expr_parser import OR
35
+
36
+ from licomp_toolkit.config import my_supported_api_version
37
+
38
+ class LicompToolkit(Licomp):
39
+ """A class implementing Licomp, but for a misc Licomp resources
40
+ and packaging the responses into a new reply
41
+ (licomp_toolkit/reply_schema.json).
42
+
43
+ LicompToolkit can check a single license agaisnt another for
44
+ compatibility, but not license expressions.
45
+ """
46
+
47
+ def __init__(self):
48
+ Licomp.__init__(self)
49
+ self._licomp_resources = {}
50
+ for licomp in [LicompReclicense, LicompOsadl]:
51
+ licomp_instance = licomp()
52
+ self._licomp_resources[licomp_instance.name()] = licomp_instance
53
+
54
+ self._licomp_resources_optional = {}
55
+ for licomp in [LicompHermione, LicompProprietary, LicompDw, GnuQuickGuideLicense, LicompOslcHandbook, LicompDoubleOpen, LicompAflc]:
56
+ licomp_instance = licomp()
57
+ self._licomp_resources_optional[licomp_instance.name()] = licomp_instance
58
+
59
+ def supported_api_version(self):
60
+ return my_supported_api_version
61
+
62
+ def __add_to_list(self, store, data, name):
63
+ if not data:
64
+ return
65
+ if data not in store:
66
+ store[data] = []
67
+ store[data].append(name)
68
+
69
+ def __add_meta(self, compatibilities):
70
+ compatibilities["meta"] = {}
71
+ compatibilities["meta"]['disclaimer'] = disclaimer
72
+
73
+ def licomp_resources(self):
74
+ return self._licomp_resources | self._licomp_resources_optional
75
+
76
+ def licomp_standard_resources(self):
77
+ return self._licomp_resources
78
+
79
+ def licomp_optional_resources(self):
80
+ return self._licomp_resources_optional
81
+
82
+ def licomp_resource_long(self, resource):
83
+ return {
84
+ 'name': resource.name(),
85
+ 'version': resource.version(),
86
+ 'usecases': [UseCase.usecase_to_string(x) for x in resource.supported_usecases()],
87
+ 'provisionings': [Provisioning.provisioning_to_string(x) for x in resource.supported_provisionings()],
88
+ 'licenses': resource.supported_licenses(),
89
+ 'type': self._resource_type(resource),
90
+ }
91
+
92
+ def licomp_resources_long(self):
93
+ _resources = []
94
+ for resource in self.licomp_resources().values():
95
+ _resources.append(self.licomp_resource_long(resource))
96
+ return _resources
97
+
98
+ def licomp_resources_short(self):
99
+ _resources = []
100
+ for resource in self.licomp_resources().values():
101
+ _resources.append(f'{resource.name()}:{resource.version()}')
102
+ return _resources
103
+
104
+ def _resource_type(self, resource):
105
+ if self._resource_is_standard(resource):
106
+ return 'standard'
107
+ return 'optional'
108
+
109
+ def _resource_is_optional(self, resource):
110
+ return resource.name() in self._licomp_resources_optional
111
+
112
+ def _resource_is_standard(self, resource):
113
+ return not self._resource_is_optional(resource)
114
+
115
+ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, provisioning, resources):
116
+ compatibilities["summary"] = {}
117
+ statuses = {}
118
+ compats = {}
119
+ compatibilities['nr_licomp'] = len(resources)
120
+ # for resource_name in self._licomp_resources():
121
+ for compat in compatibilities["compatibilities"]:
122
+ logging.debug(f': {compat}')
123
+ logging.debug(f': {compat["resource_name"]}')
124
+ self.__add_to_list(statuses, compat['status'], compat)
125
+ self.__add_to_list(compats, compat['compatibility_status'], compat)
126
+ compatibilities["summary"]["resources"] = self.licomp_resources_short()
127
+ compatibilities["summary"]["outbound"] = outbound
128
+ compatibilities["summary"]["inbound"] = inbound
129
+ compatibilities["summary"]["usecase"] = UseCase.usecase_to_string(usecase)
130
+ compatibilities["summary"]["provisioning"] = Provisioning.provisioning_to_string(provisioning)
131
+ compatibilities["summary"]["statuses"] = statuses
132
+ compatibilities["summary"]["compatibility_statuses"] = compats
133
+
134
+ compat_number = len(compatibilities["summary"]["statuses"].get("success", []))
135
+ logging.debug(f': {compatibilities["summary"]["statuses"]}')
136
+ results = {}
137
+ results['nr_valid'] = f'{compat_number}'
138
+ for key, value in compatibilities["summary"]["compatibility_statuses"].items():
139
+ logging.debug(f': {len(value)}/{compat_number}')
140
+ if compat_number == 0:
141
+ continue
142
+ else:
143
+ if key == 'unsupported':
144
+ continue
145
+ else:
146
+ count = len(value)
147
+ perc = len(value) / compat_number * 100
148
+ results[key] = {
149
+ 'count': count,
150
+ 'percent': perc,
151
+ }
152
+ compatibilities['summary']['results'] = results
153
+
154
+ # override top class
155
+ def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning, resources=None):
156
+ logging.debug(f'{inbound} {outbound} ')
157
+
158
+ compatibilities = {}
159
+ compatibilities['compatibilities'] = []
160
+
161
+ if not resources:
162
+ resources = self.licomp_resources().keys()
163
+
164
+ for resource_name in resources:
165
+ resource = self.licomp_resources()[resource_name]
166
+ logging.debug(f'-- resource: {resource.name()}')
167
+
168
+ compat = resource.outbound_inbound_compatibility(outbound, inbound, usecase, provisioning=provisioning)
169
+ compatibilities['compatibilities'].append(compat)
170
+
171
+ self.__summarize_compatibility(compatibilities, outbound, inbound, usecase, provisioning, resources)
172
+ self.__add_meta(compatibilities)
173
+
174
+ return compatibilities
175
+
176
+ def simplify(self, lic):
177
+ return str(Licensing([]).parse(lic).simplify())
178
+
179
+ def supported_licenses(self):
180
+ licenses = set()
181
+ for resource in self.licomp_resources().values():
182
+ licenses.update(set(resource.supported_licenses()))
183
+ licenses = list(licenses)
184
+ licenses.sort()
185
+ return licenses
186
+
187
+ def supported_provisionings(self):
188
+ provisionings = set()
189
+ for resource in self.licomp_resources().values():
190
+ provisionings.update(set(resource.supported_provisionings()))
191
+ return list(provisionings)
192
+
193
+ def supported_usecases(self):
194
+ usecases = set()
195
+ for resource in self.licomp_resources().values():
196
+ usecases.update(set(resource.supported_usecases()))
197
+ return list(usecases)
198
+
199
+ def disclaimer(self):
200
+ return disclaimer
201
+
202
+ def version(self, verbose=False):
203
+ return licomp_toolkit_version
204
+
205
+ def versions(self, verbose=False):
206
+ resources = {}
207
+ for resource in self.licomp_resources().values():
208
+ resources[resource.name()] = resource.version()
209
+ return {
210
+ self.name(): self.version(),
211
+ 'licomp-resources': resources,
212
+ }
213
+
214
+ def name(self):
215
+ return cli_name
216
+
217
+ class LicenseExpressionChecker():
218
+ """This class can check compatibility between a single outbound
219
+ license (e.g GPL-2.0-only) against an inbound license expression
220
+ (e.g. MIT OR X11)
221
+ """
222
+
223
+ def __init__(self):
224
+ self.le_parser = LicenseExpressionParser()
225
+ self.licomp_toolkit = LicompToolkit()
226
+
227
+ def __compatibility_status(self, compatibility):
228
+ status = compatibility['summary']['results']
229
+ rets = []
230
+ for ret in status:
231
+ if ret == 'nr_valid':
232
+ continue
233
+ elif not ret:
234
+ pass
235
+ elif ret == CompatibilityStatus.compat_status_to_string(CompatibilityStatus.UNSUPPORTED):
236
+ pass
237
+ else:
238
+ rets.append(ret)
239
+
240
+ if len(rets) == 0:
241
+ return CompatibilityStatus.compat_status_to_string(CompatibilityStatus.UNSUPPORTED)
242
+
243
+ if len(rets) == 1:
244
+ return rets[0]
245
+
246
+ return CompatibilityStatus.compat_status_to_string(CompatibilityStatus.MIXED)
247
+
248
+ def check_compatibility(self,
249
+ outbound,
250
+ parsed_expression,
251
+ usecase,
252
+ provisioning,
253
+ resources,
254
+ detailed_report=True):
255
+
256
+ compat_object = {
257
+ 'compatibility_check': f'outbound-{self.le_parser.parse_license_expression(outbound)["compatibility_type"]} -> inbound-{parsed_expression["compatibility_type"]}',
258
+ 'check_class': __class__.__name__,
259
+ }
260
+
261
+ if parsed_expression[COMPATIBILITY_TYPE] == 'license':
262
+ compat_object['compatibility_check'] = 'outbound-license -> inbound-license'
263
+ lic = parsed_expression['license']
264
+ compat = self.licomp_toolkit.outbound_inbound_compatibility(outbound,
265
+ lic,
266
+ usecase,
267
+ provisioning,
268
+ resources)
269
+ compat_object['compatibility'] = self.__compatibility_status(compat)
270
+ if detailed_report:
271
+ compat_object['compatibility_details'] = compat
272
+ else:
273
+ compat_object['compatibility_details'] = None
274
+ compat_object['inbound_license'] = lic
275
+ compat_object['outbound_license'] = outbound
276
+ compat_object['compatibility_object'] = {}
277
+
278
+ else:
279
+ operator = parsed_expression['operator']
280
+ operands = parsed_expression['operands']
281
+ compat_object['operator'] = operator
282
+
283
+ compat_object['inbound_license'] = self.le_parser.to_string(parsed_expression)
284
+ compat_object['outbound_license'] = outbound
285
+ compat_object['compatibility_details'] = None
286
+ operands_object = []
287
+ for operand in operands:
288
+ operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, resources, detailed_report=detailed_report)
289
+ operand_object = {
290
+ 'compatibility_object': operand_compat,
291
+ 'compatibility': operand_compat['compatibility'],
292
+ }
293
+ operands_object.append(operand_object)
294
+
295
+ compat_object['compatibility'] = self.summarise_compatibilities(operator, operands_object)
296
+ compat_object['operands'] = operands_object
297
+
298
+ return compat_object
299
+
300
+ def __init_summary(self, operands):
301
+ summary = {
302
+ "yes": 0,
303
+ "no": 0,
304
+ "depends": 0,
305
+ "unknown": 0,
306
+ "unsupported": 0,
307
+ "mixed": 0,
308
+ }
309
+ for operand in operands:
310
+ compat = operand['compatibility']
311
+ summary[compat] = summary[compat] + 1
312
+ return summary
313
+
314
+ def __summarise_compatibilities_and(self, operands):
315
+ nr_operands = len(operands)
316
+ summary = self.__init_summary(operands)
317
+
318
+ if summary['no'] != 0:
319
+ return 'no'
320
+
321
+ if summary['yes'] == nr_operands:
322
+ return "yes"
323
+
324
+ return "no"
325
+
326
+ def __summarise_compatibilities_or(self, operands):
327
+ summary = self.__init_summary(operands)
328
+
329
+ if summary['yes'] != 0:
330
+ return 'yes'
331
+
332
+ return "no"
333
+
334
+ def summarise_compatibilities(self, operator, operands):
335
+ return {
336
+ AND: self.__summarise_compatibilities_and,
337
+ OR: self.__summarise_compatibilities_or,
338
+ }[operator](operands)
339
+
340
+
341
+ class ExpressionExpressionChecker():
342
+ """
343
+ This class can check, for compatibility;
344
+ * inbound license expression (e.g. MIT OR Apache-2.0)
345
+ * against outbound license expression (e.g. GPL-2.0-only OR BSD-2-Clause)
346
+ """
347
+
348
+ def __init__(self):
349
+ self.le_checker = LicenseExpressionChecker()
350
+ self.le_parser = LicenseExpressionParser()
351
+ self.licomp_toolkit = LicompToolkit()
352
+
353
+ def __parsed_expression_to_name(self, parsed_expression):
354
+ return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]]
355
+
356
+ def meta_information(self):
357
+ return {
358
+ 'tool': module_name,
359
+ 'file': 'verification',
360
+ 'file_version': licomp_toolkit_file_version,
361
+ }
362
+
363
+ def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=None, detailed_report=True):
364
+
365
+ # Check usecase
366
+ try:
367
+ usecase_obj = UseCase.string_to_usecase(usecase)
368
+ except KeyError:
369
+ raise LicompException(f'Usecase {usecase} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_USECASE)
370
+
371
+ # Check provisioning
372
+ try:
373
+ provisioning_obj = Provisioning.string_to_provisioning(provisioning)
374
+ except KeyError:
375
+ raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING)
376
+
377
+ licomp_resources = list(self.licomp_toolkit.licomp_standard_resources().keys())
378
+ if not resources:
379
+ resources = licomp_resources
380
+ else:
381
+ resources = resources
382
+
383
+ unavailable_resources = []
384
+
385
+ for resource in resources:
386
+ resource_object = self.licomp_toolkit.licomp_resources()[resource]
387
+ unavailable_reasons = []
388
+
389
+ # is usecase supported by resource
390
+ if not resource_object.usecase_supported(UseCase.string_to_usecase(usecase)):
391
+ unavailable_reasons.append(f'Usecase "{usecase}" not supported')
392
+
393
+ # is prov case supported by resource
394
+ if not resource_object.provisioning_supported(Provisioning.string_to_provisioning(provisioning)):
395
+ unavailable_reasons.append(f'Provisioning case "{provisioning}" not supported')
396
+
397
+ if unavailable_reasons:
398
+ unavailable_resources.append({
399
+ "resource": resource,
400
+ 'reasons': ", ".join(unavailable_reasons),
401
+ })
402
+
403
+ unavailable_resource_keys = [resource['resource'] for resource in unavailable_resources]
404
+ available_resources = [resource for resource in resources if resource not in unavailable_resource_keys]
405
+
406
+ inbound_parsed = self.le_parser.parse_license_expression(inbound)
407
+ outbound_parsed = self.le_parser.parse_license_expression(outbound)
408
+ compatibility_object = self.__check_compatibility(outbound_parsed,
409
+ inbound_parsed,
410
+ usecase_obj,
411
+ provisioning_obj,
412
+ resources,
413
+ detailed_report)
414
+ return {
415
+ 'meta': self.meta_information(),
416
+ 'inbound': str(inbound),
417
+ 'outbound': str(outbound),
418
+ 'usecase': usecase,
419
+ 'resources': resources,
420
+ 'provisioning': provisioning,
421
+ 'compatibility': compatibility_object['compatibility'],
422
+ 'compatibility_report': compatibility_object,
423
+ 'unavailable_resources': unavailable_resources,
424
+ 'available_resources': available_resources,
425
+ }
426
+
427
+ def __check_compatibility(self,
428
+ outbound_parsed,
429
+ inbound_parsed,
430
+ usecase,
431
+ provisioning,
432
+ resources,
433
+ detailed_report=True):
434
+
435
+ outbound_type = outbound_parsed[COMPATIBILITY_TYPE]
436
+ compat_object = {
437
+ 'inbound_license': self.le_parser.to_string(inbound_parsed),
438
+ 'outbound_license': self.le_parser.to_string(outbound_parsed),
439
+ 'check_class': __class__.__name__,
440
+ }
441
+
442
+ if outbound_type == 'license':
443
+ if False:
444
+ compat_object['compatibility_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]} HERE?'
445
+ outbound_parsed_license = outbound_parsed['license']
446
+
447
+ compat = self.le_checker.check_compatibility(outbound_parsed_license,
448
+ inbound_parsed,
449
+ usecase,
450
+ provisioning,
451
+ resources,
452
+ detailed_report)
453
+ compat_object['compatibility'] = compat['compatibility']
454
+ compat_object['compatibility_object'] = compat
455
+ compat_object['compatibility_details'] = None
456
+
457
+ outbound_parsed_license = outbound_parsed['license']
458
+ compat = self.le_checker.check_compatibility(outbound_parsed_license,
459
+ inbound_parsed,
460
+ usecase,
461
+ provisioning,
462
+ resources,
463
+ detailed_report)
464
+ compat_object = compat
465
+
466
+ elif outbound_type == 'expression':
467
+ compat_object['compatibility_details'] = None
468
+ compat_object['compatibility_check'] = f'outbound-expression -> inbound-{inbound_parsed["compatibility_type"]}'
469
+ operator = outbound_parsed['operator']
470
+ operands = outbound_parsed['operands']
471
+
472
+ compat_object['operator'] = operator
473
+
474
+ operands_object = []
475
+ for operand in operands:
476
+ # Check if:
477
+ # operand from outbound license
478
+ # is compatible with
479
+ # inbound license
480
+ operand_compat = self.__check_compatibility(operand,
481
+ inbound_parsed,
482
+ usecase,
483
+ provisioning,
484
+ resources,
485
+ detailed_report)
486
+ operand_object = {
487
+ 'compatibility_object': operand_compat,
488
+ 'compatibility': operand_compat['compatibility'],
489
+ }
490
+ operands_object.append(operand_object)
491
+
492
+ compat_object['compatibility'] = self.le_checker.summarise_compatibilities(operator, operands_object)
493
+ compat_object['operands'] = operands_object
494
+
495
+ return compat_object
496
+
497
+ def simplify(self, compat):
498
+ return {
499
+ 'outbound': compat['outbound'],
500
+ 'inbound': compat['inbound'],
501
+ 'usecase': compat['usecase'],
502
+ 'provisioning': compat['provisioning'],
503
+ 'compatibility': compat['compatibility'],
504
+ 'resources': compat['available_resources'],
505
+ }