scanoss 1.20.5__py3-none-any.whl → 1.22.0__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.
Files changed (48) hide show
  1. protoc_gen_swagger/options/annotations_pb2.py +9 -12
  2. protoc_gen_swagger/options/annotations_pb2_grpc.py +1 -1
  3. protoc_gen_swagger/options/openapiv2_pb2.py +96 -98
  4. protoc_gen_swagger/options/openapiv2_pb2_grpc.py +1 -1
  5. scanoss/__init__.py +1 -1
  6. scanoss/api/common/v2/scanoss_common_pb2.py +20 -18
  7. scanoss/api/common/v2/scanoss_common_pb2_grpc.py +1 -1
  8. scanoss/api/components/v2/scanoss_components_pb2.py +38 -48
  9. scanoss/api/components/v2/scanoss_components_pb2_grpc.py +96 -142
  10. scanoss/api/cryptography/v2/scanoss_cryptography_pb2.py +42 -22
  11. scanoss/api/cryptography/v2/scanoss_cryptography_pb2_grpc.py +185 -75
  12. scanoss/api/dependencies/v2/scanoss_dependencies_pb2.py +32 -30
  13. scanoss/api/dependencies/v2/scanoss_dependencies_pb2_grpc.py +83 -75
  14. scanoss/api/provenance/v2/scanoss_provenance_pb2.py +20 -21
  15. scanoss/api/provenance/v2/scanoss_provenance_pb2_grpc.py +1 -1
  16. scanoss/api/scanning/v2/scanoss_scanning_pb2.py +20 -10
  17. scanoss/api/scanning/v2/scanoss_scanning_pb2_grpc.py +70 -40
  18. scanoss/api/semgrep/v2/scanoss_semgrep_pb2.py +18 -22
  19. scanoss/api/semgrep/v2/scanoss_semgrep_pb2_grpc.py +49 -71
  20. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2.py +27 -37
  21. scanoss/api/vulnerabilities/v2/scanoss_vulnerabilities_pb2_grpc.py +72 -109
  22. scanoss/cli.py +417 -74
  23. scanoss/components.py +5 -3
  24. scanoss/constants.py +12 -0
  25. scanoss/data/build_date.txt +1 -1
  26. scanoss/file_filters.py +272 -57
  27. scanoss/results.py +92 -109
  28. scanoss/scanner.py +25 -20
  29. scanoss/scanners/__init__.py +23 -0
  30. scanoss/scanners/container_scanner.py +474 -0
  31. scanoss/scanners/folder_hasher.py +302 -0
  32. scanoss/scanners/scanner_config.py +73 -0
  33. scanoss/scanners/scanner_hfh.py +172 -0
  34. scanoss/scanoss_settings.py +9 -5
  35. scanoss/scanossapi.py +29 -7
  36. scanoss/scanossbase.py +9 -3
  37. scanoss/scanossgrpc.py +145 -13
  38. scanoss/threadedscanning.py +6 -6
  39. scanoss/utils/abstract_presenter.py +103 -0
  40. scanoss/utils/crc64.py +96 -0
  41. scanoss/utils/simhash.py +198 -0
  42. {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info}/METADATA +4 -2
  43. scanoss-1.22.0.dist-info/RECORD +83 -0
  44. {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info}/WHEEL +1 -1
  45. scanoss-1.20.5.dist-info/RECORD +0 -74
  46. {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info}/entry_points.txt +0 -0
  47. {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info/licenses}/LICENSE +0 -0
  48. {scanoss-1.20.5.dist-info → scanoss-1.22.0.dist-info}/top_level.txt +0 -0
scanoss/results.py CHANGED
@@ -25,6 +25,8 @@ SPDX-License-Identifier: MIT
25
25
  import json
26
26
  from typing import Any, Dict, List
27
27
 
28
+ from scanoss.utils.abstract_presenter import AbstractPresenter
29
+
28
30
  from .scanossbase import ScanossBase
29
31
 
30
32
  MATCH_TYPES = ['file', 'snippet']
@@ -47,16 +49,89 @@ PENDING_IDENTIFICATION_FILTERS = {
47
49
  'status': ['pending'],
48
50
  }
49
51
 
50
- AVAILABLE_OUTPUT_FORMATS = ['json', 'plain']
51
52
 
53
+ class ResultsPresenter(AbstractPresenter):
54
+ """
55
+ SCANOSS Results presenter class
56
+ Handles the presentation of the scan results
57
+ """
58
+
59
+ def __init__(self, results_instance, **kwargs):
60
+ super().__init__(**kwargs)
61
+ self.results = results_instance
62
+
63
+ def _format_json_output(self) -> str:
64
+ """
65
+ Format the output data into a JSON object
66
+ """
67
+
68
+ formatted_data = []
69
+ for item in self.results.data:
70
+ formatted_data.append(
71
+ {
72
+ 'file': item.get('filename'),
73
+ 'status': item.get('status', 'N/A'),
74
+ 'match_type': item['id'],
75
+ 'matched': item.get('matched', 'N/A'),
76
+ 'purl': (item.get('purl')[0] if item.get('purl') else 'N/A'),
77
+ 'license': (item.get('licenses')[0].get('name', 'N/A') if item.get('licenses') else 'N/A'),
78
+ }
79
+ )
80
+ try:
81
+ return json.dumps({'results': formatted_data, 'total': len(formatted_data)}, indent=2)
82
+ except Exception as e:
83
+ self.base.print_stderr(f'ERROR: Problem formatting JSON output: {e}')
84
+ return ''
85
+
86
+ def _format_cyclonedx_output(self) -> str:
87
+ raise NotImplementedError('CycloneDX output is not implemented')
88
+
89
+ def _format_spdxlite_output(self) -> str:
90
+ raise NotImplementedError('SPDXlite output is not implemented')
91
+
92
+ def _format_csv_output(self) -> str:
93
+ raise NotImplementedError('CSV output is not implemented')
94
+
95
+ def _format_raw_output(self) -> str:
96
+ raise NotImplementedError('Raw output is not implemented')
97
+
98
+ def _format_plain_output(self) -> str:
99
+ """Format the output data into a plain text string
100
+
101
+ Returns:
102
+ str: The formatted output data
103
+ """
104
+ if not self.results.data:
105
+ msg = 'No results to present'
106
+ return msg
52
107
 
53
- class Results(ScanossBase):
108
+ formatted = ''
109
+ for item in self.results.data:
110
+ formatted += f'{self._format_plain_output_item(item)}\n'
111
+ return formatted
112
+
113
+ @staticmethod
114
+ def _format_plain_output_item(item):
115
+ purls = item.get('purl', [])
116
+ licenses = item.get('licenses', [])
117
+
118
+ return (
119
+ f'File: {item.get("filename")}\n'
120
+ f'Match type: {item.get("id")}\n'
121
+ f'Status: {item.get("status", "N/A")}\n'
122
+ f'Matched: {item.get("matched", "N/A")}\n'
123
+ f'Purl: {purls[0] if purls else "N/A"}\n'
124
+ f'License: {licenses[0].get("name", "N/A") if licenses else "N/A"}\n'
125
+ )
126
+
127
+
128
+ class Results:
54
129
  """
55
130
  SCANOSS Results class \n
56
131
  Handles the parsing and filtering of the scan results
57
132
  """
58
133
 
59
- def __init__(
134
+ def __init__( # noqa: PLR0913
60
135
  self,
61
136
  debug: bool = False,
62
137
  trace: bool = False,
@@ -80,11 +155,17 @@ class Results(ScanossBase):
80
155
  output_format (str, optional): Output format. Defaults to None.
81
156
  """
82
157
 
83
- super().__init__(debug, trace, quiet)
158
+ self.base = ScanossBase(debug, trace, quiet)
84
159
  self.data = self._load_and_transform(filepath)
85
160
  self.filters = self._load_filters(match_type=match_type, status=status)
86
- self.output_file = output_file
87
- self.output_format = output_format
161
+ self.presenter = ResultsPresenter(
162
+ self,
163
+ debug=debug,
164
+ trace=trace,
165
+ quiet=quiet,
166
+ output_file=output_file,
167
+ output_format=output_format,
168
+ )
88
169
 
89
170
  def load_file(self, file: str) -> Dict[str, Any]:
90
171
  """Load the JSON file
@@ -99,7 +180,7 @@ class Results(ScanossBase):
99
180
  try:
100
181
  return json.load(jsonfile)
101
182
  except Exception as e:
102
- self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
183
+ self.base.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
103
184
 
104
185
  def _load_and_transform(self, file: str) -> List[Dict[str, Any]]:
105
186
  """
@@ -174,8 +255,8 @@ class Results(ScanossBase):
174
255
  def _validate_filter_values(filter_key: str, filter_value: List[str]):
175
256
  if any(value not in AVAILABLE_FILTER_VALUES.get(filter_key, []) for value in filter_value):
176
257
  valid_values = ', '.join(AVAILABLE_FILTER_VALUES.get(filter_key, []))
177
- raise Exception(
178
- f"ERROR: Invalid filter value '{filter_value}' for filter '{filter_key.value}'. "
258
+ raise ValueError(
259
+ f"ERROR: Invalid filter value '{filter_value}' for filter '{filter_key}'. "
179
260
  f'Valid values are: {valid_values}'
180
261
  )
181
262
 
@@ -190,103 +271,5 @@ class Results(ScanossBase):
190
271
  return bool(self.data)
191
272
 
192
273
  def present(self, output_format: str = None, output_file: str = None):
193
- """Format and present the results. If no output format is provided, the results will be printed to stdout
194
-
195
- Args:
196
- output_format (str, optional): Output format. Defaults to None.
197
- output_file (str, optional): Output file. Defaults to None.
198
-
199
- Raises:
200
- Exception: Invalid output format
201
-
202
- Returns:
203
- None
204
- """
205
- file_path = output_file or self.output_file
206
- fmt = output_format or self.output_format
207
-
208
- if fmt and fmt not in AVAILABLE_OUTPUT_FORMATS:
209
- raise Exception(
210
- f"ERROR: Invalid output format '{output_format}'. Valid values are: {', '.join(AVAILABLE_OUTPUT_FORMATS)}"
211
- )
212
-
213
- if fmt == 'json':
214
- return self._present_json(file_path)
215
- elif fmt == 'plain':
216
- return self._present_plain(file_path)
217
- else:
218
- return self._present_stdout()
219
-
220
- def _present_json(self, file: str = None):
221
- """Present the results in JSON format
222
-
223
- Args:
224
- file (str, optional): Output file. Defaults to None.
225
- """
226
- self.print_to_file_or_stdout(json.dumps(self._format_json_output(), indent=2), file)
227
-
228
- def _format_json_output(self):
229
- """
230
- Format the output data into a JSON object
231
- """
232
-
233
- formatted_data = []
234
- for item in self.data:
235
- formatted_data.append(
236
- {
237
- 'file': item.get('filename'),
238
- 'status': item.get('status', 'N/A'),
239
- 'match_type': item['id'],
240
- 'matched': item.get('matched', 'N/A'),
241
- 'purl': (item.get('purl')[0] if item.get('purl') else 'N/A'),
242
- 'license': (item.get('licenses')[0].get('name', 'N/A') if item.get('licenses') else 'N/A'),
243
- }
244
- )
245
- return {'results': formatted_data, 'total': len(formatted_data)}
246
-
247
- def _present_plain(self, file: str = None):
248
- """Present the results in plain text format
249
-
250
- Args:
251
- file (str, optional): Output file. Defaults to None.
252
-
253
- Returns:
254
- None
255
- """
256
- if not self.data:
257
- return self.print_stderr('No results to present')
258
- self.print_to_file_or_stdout(self._format_plain_output(), file)
259
-
260
- def _present_stdout(self):
261
- """Present the results to stdout
262
-
263
- Returns:
264
- None
265
- """
266
- if not self.data:
267
- return self.print_stderr('No results to present')
268
- self.print_to_file_or_stdout(self._format_plain_output())
269
-
270
- def _format_plain_output(self):
271
- """
272
- Format the output data into a plain text string
273
- """
274
-
275
- formatted = ''
276
- for item in self.data:
277
- formatted += f'{self._format_plain_output_item(item)} \n'
278
- return formatted
279
-
280
- @staticmethod
281
- def _format_plain_output_item(item):
282
- purls = item.get('purl', [])
283
- licenses = item.get('licenses', [])
284
-
285
- return (
286
- f'File: {item.get("filename")}\n'
287
- f'Match type: {item.get("id")}\n'
288
- f'Status: {item.get("status", "N/A")}\n'
289
- f'Matched: {item.get("matched", "N/A")}\n'
290
- f'Purl: {purls[0] if purls else "N/A"}\n'
291
- f'License: {licenses[0].get("name", "N/A") if licenses else "N/A"}\n'
292
- )
274
+ """Present the results in the selected format"""
275
+ self.presenter.present(output_format=output_format, output_file=output_file)
scanoss/scanner.py CHANGED
@@ -69,7 +69,7 @@ class Scanner(ScanossBase):
69
69
  Handle the scanning of files, snippets and dependencies
70
70
  """
71
71
 
72
- def __init__(
72
+ def __init__( # noqa: PLR0913, PLR0915
73
73
  self,
74
74
  wfp: str = None,
75
75
  scan_output: str = None,
@@ -106,6 +106,7 @@ class Scanner(ScanossBase):
106
106
  strip_snippet_ids=None,
107
107
  skip_md5_ids=None,
108
108
  scan_settings: 'ScanossSettings | None' = None,
109
+ req_headers: dict = None,
109
110
  ):
110
111
  """
111
112
  Initialise scanning class, including Winnowing, ScanossApi, ThreadedScanning
@@ -129,6 +130,7 @@ class Scanner(ScanossBase):
129
130
  self.skip_folders = skip_folders
130
131
  self.skip_size = skip_size
131
132
  self.skip_extensions = skip_extensions
133
+ self.req_headers = req_headers
132
134
  ver_details = Scanner.version_details()
133
135
 
134
136
  self.winnowing = Winnowing(
@@ -156,6 +158,7 @@ class Scanner(ScanossBase):
156
158
  ca_cert=ca_cert,
157
159
  pac=pac,
158
160
  retry=retry,
161
+ req_headers= self.req_headers,
159
162
  )
160
163
  sc_deps = ScancodeDeps(debug=debug, quiet=quiet, trace=trace, timeout=sc_timeout, sc_command=sc_command)
161
164
  grpc_api = ScanossGrpc(
@@ -169,6 +172,7 @@ class Scanner(ScanossBase):
169
172
  proxy=proxy,
170
173
  pac=pac,
171
174
  grpc_proxy=grpc_proxy,
175
+ req_headers=self.req_headers,
172
176
  )
173
177
  self.threaded_deps = ThreadedDependencies(sc_deps, grpc_api, debug=debug, quiet=quiet, trace=trace)
174
178
  self.nb_threads = nb_threads
@@ -302,7 +306,7 @@ class Scanner(ScanossBase):
302
306
 
303
307
  success = True
304
308
  if not scan_dir:
305
- raise Exception(f'ERROR: Please specify a folder to scan')
309
+ raise Exception('ERROR: Please specify a folder to scan')
306
310
  if not os.path.exists(scan_dir) or not os.path.isdir(scan_dir):
307
311
  raise Exception(f'ERROR: Specified folder does not exist or is not a folder: {scan_dir}')
308
312
  if not self.is_file_or_snippet_scan() and not self.is_dependency_scan():
@@ -386,7 +390,8 @@ class Scanner(ScanossBase):
386
390
  file_count += 1
387
391
  if self.threaded_scan:
388
392
  wfp_size = len(wfp.encode('utf-8'))
389
- # If the WFP is bigger than the max post size and we already have something stored in the scan block, add it to the queue
393
+ # If the WFP is bigger than the max post size and we already have something stored in the scan block,
394
+ # add it to the queue
390
395
  if scan_block != '' and (wfp_size + scan_size) >= self.max_post_size:
391
396
  self.threaded_scan.queue_add(scan_block)
392
397
  queue_size += 1
@@ -436,7 +441,7 @@ class Scanner(ScanossBase):
436
441
  self.threaded_scan.update_bar(create=True, file_count=file_count)
437
442
  if not scan_started:
438
443
  if not self.threaded_scan.run(wait=False): # Run the scan but do not wait for it to complete
439
- self.print_stderr(f'Warning: Some errors encounted while scanning. Results might be incomplete.')
444
+ self.print_stderr('Warning: Some errors encounted while scanning. Results might be incomplete.')
440
445
  success = False
441
446
  return success
442
447
 
@@ -457,14 +462,14 @@ class Scanner(ScanossBase):
457
462
  dep_responses = None
458
463
  if self.is_file_or_snippet_scan():
459
464
  if not self.threaded_scan.complete(): # Wait for the scans to complete
460
- self.print_stderr(f'Warning: Scanning analysis ran into some trouble.')
465
+ self.print_stderr('Warning: Scanning analysis ran into some trouble.')
461
466
  success = False
462
467
  self.threaded_scan.complete_bar()
463
468
  scan_responses = self.threaded_scan.responses
464
469
  if self.is_dependency_scan():
465
470
  self.print_msg('Retrieving dependency data...')
466
471
  if not self.threaded_deps.complete():
467
- self.print_stderr(f'Warning: Dependency analysis ran into some trouble.')
472
+ self.print_stderr('Warning: Dependency analysis ran into some trouble.')
468
473
  success = False
469
474
  dep_responses = self.threaded_deps.responses
470
475
 
@@ -546,7 +551,7 @@ class Scanner(ScanossBase):
546
551
  """
547
552
  success = True
548
553
  if not file:
549
- raise Exception(f'ERROR: Please specify a file to scan')
554
+ raise Exception('ERROR: Please specify a file to scan')
550
555
  if not os.path.exists(file) or not os.path.isfile(file):
551
556
  raise Exception(f'ERROR: Specified file does not exist or is not a file: {file}')
552
557
  if not self.is_file_or_snippet_scan() and not self.is_dependency_scan():
@@ -583,7 +588,7 @@ class Scanner(ScanossBase):
583
588
  """
584
589
  success = True
585
590
  if not file:
586
- raise Exception(f'ERROR: Please specify a file to scan')
591
+ raise Exception('ERROR: Please specify a file to scan')
587
592
  if not os.path.exists(file) or not os.path.isfile(file):
588
593
  raise Exception(f'ERROR: Specified files does not exist or is not a file: {file}')
589
594
  self.print_debug(f'Fingerprinting {file}...')
@@ -608,7 +613,7 @@ class Scanner(ScanossBase):
608
613
  """
609
614
  success = True
610
615
  if not files:
611
- raise Exception(f'ERROR: Please provide a non-empty list of filenames to scan')
616
+ raise Exception('ERROR: Please provide a non-empty list of filenames to scan')
612
617
 
613
618
  file_filters = FileFilters(
614
619
  debug=self.debug,
@@ -671,7 +676,7 @@ class Scanner(ScanossBase):
671
676
  scan_started = True
672
677
  if not self.threaded_scan.run(wait=False):
673
678
  self.print_stderr(
674
- f'Warning: Some errors encounted while scanning. Results might be incomplete.'
679
+ 'Warning: Some errors encounted while scanning. Results might be incomplete.'
675
680
  )
676
681
  success = False
677
682
 
@@ -704,12 +709,12 @@ class Scanner(ScanossBase):
704
709
  """
705
710
  success = True
706
711
  if not files:
707
- raise Exception(f'ERROR: Please specify a list of files to scan')
712
+ raise Exception('ERROR: Please specify a list of files to scan')
708
713
  if not self.is_file_or_snippet_scan():
709
714
  raise Exception(f'ERROR: file or snippet scan options have to be set to scan files: {files}')
710
715
  if self.is_dependency_scan() or deps_file:
711
716
  raise Exception(
712
- f'ERROR: The dependency scan option is currently not supported when scanning a list of files'
717
+ 'ERROR: The dependency scan option is currently not supported when scanning a list of files'
713
718
  )
714
719
  if self.scan_output:
715
720
  self.print_msg(f'Writing results to {self.scan_output}...')
@@ -731,9 +736,9 @@ class Scanner(ScanossBase):
731
736
  """
732
737
  success = True
733
738
  if not filename:
734
- raise Exception(f'ERROR: Please specify a filename to scan')
739
+ raise Exception('ERROR: Please specify a filename to scan')
735
740
  if not contents:
736
- raise Exception(f'ERROR: Please specify a file contents to scan')
741
+ raise Exception('ERROR: Please specify a file contents to scan')
737
742
 
738
743
  self.print_debug(f'Fingerprinting {filename}...')
739
744
  wfp = self.winnowing.wfp_for_contents(filename, False, contents)
@@ -924,7 +929,7 @@ class Scanner(ScanossBase):
924
929
  scan_started = True
925
930
  if not self.threaded_scan.run(wait=False):
926
931
  self.print_stderr(
927
- f'Warning: Some errors encounted while scanning. Results might be incomplete.'
932
+ 'Warning: Some errors uncounted while scanning. Results might be incomplete.'
928
933
  )
929
934
  success = False
930
935
  # End for loop
@@ -948,7 +953,7 @@ class Scanner(ScanossBase):
948
953
  """
949
954
  success = True
950
955
  if not wfp:
951
- raise Exception(f'ERROR: Please specify a WFP to scan')
956
+ raise Exception('ERROR: Please specify a WFP to scan')
952
957
  raw_output = '{\n'
953
958
  scan_resp = self.scanoss_api.scan(wfp)
954
959
  if scan_resp is not None:
@@ -984,9 +989,9 @@ class Scanner(ScanossBase):
984
989
  :return:
985
990
  """
986
991
  if not filename:
987
- raise Exception(f'ERROR: Please specify a filename to scan')
992
+ raise Exception('ERROR: Please specify a filename to scan')
988
993
  if not contents:
989
- raise Exception(f'ERROR: Please specify a file contents to scan')
994
+ raise Exception('ERROR: Please specify a file contents to scan')
990
995
 
991
996
  self.print_debug(f'Fingerprinting {filename}...')
992
997
  wfp = self.winnowing.wfp_for_contents(filename, False, contents)
@@ -1005,7 +1010,7 @@ class Scanner(ScanossBase):
1005
1010
  Fingerprint the specified file
1006
1011
  """
1007
1012
  if not scan_file:
1008
- raise Exception(f'ERROR: Please specify a file to fingerprint')
1013
+ raise Exception('ERROR: Please specify a file to fingerprint')
1009
1014
  if not os.path.exists(scan_file) or not os.path.isfile(scan_file):
1010
1015
  raise Exception(f'ERROR: Specified file does not exist or is not a file: {scan_file}')
1011
1016
 
@@ -1026,7 +1031,7 @@ class Scanner(ScanossBase):
1026
1031
  Fingerprint the specified folder producing fingerprints
1027
1032
  """
1028
1033
  if not scan_dir:
1029
- raise Exception(f'ERROR: Please specify a folder to fingerprint')
1034
+ raise Exception('ERROR: Please specify a folder to fingerprint')
1030
1035
  if not os.path.exists(scan_dir) or not os.path.isdir(scan_dir):
1031
1036
  raise Exception(f'ERROR: Specified folder does not exist or is not a folder: {scan_dir}')
1032
1037
  file_filters = FileFilters(
@@ -0,0 +1,23 @@
1
+ """
2
+ SPDX-License-Identifier: MIT
3
+
4
+ Copyright (c) 2025, SCANOSS
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
23
+ """