msfabricpysdkcore 0.0.1__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,81 @@
1
+ import json
2
+ import requests
3
+ from time import sleep
4
+
5
+ class LongRunningOperation:
6
+ """Class to represent a workspace in Microsoft Fabric"""
7
+
8
+ def __init__(self, operation_id, auth) -> None:
9
+ self.operation_id = operation_id
10
+ self.auth = auth
11
+
12
+ self.state = self.get_operation_state()["status"]
13
+
14
+
15
+ def get_operation_results(self):
16
+ """Get the results of an operation"""
17
+ url = f"https://api.fabric.microsoft.com/v1/operations/{self.operation_id}/result"
18
+
19
+ for _ in range(10):
20
+ response = requests.get(url=url, headers=self.auth.get_headers())
21
+ if response.status_code == 429:
22
+ print("Too many requests, waiting 10 seconds")
23
+ sleep(10)
24
+ continue
25
+ if response.status_code not in (200, 429):
26
+ print(response.status_code)
27
+ print(response.text)
28
+ raise Exception(f"Error getting operation results: {response.text}")
29
+ break
30
+
31
+ print(json.loads(response.text))
32
+ return json.loads(response.text)
33
+
34
+ def get_operation_state(self):
35
+ """Get the state of an operation"""
36
+ url = f"https://api.fabric.microsoft.com/v1/operations/{self.operation_id}"
37
+
38
+ for _ in range(10):
39
+ response = requests.get(url=url, headers=self.auth.get_headers())
40
+ if response.status_code == 429:
41
+ print("Too many requests, waiting 10 seconds")
42
+ sleep(10)
43
+ continue
44
+ if response.status_code not in (200, 429):
45
+ print(response.status_code)
46
+ print(response.text)
47
+ raise Exception(f"Error getting operation state: {response.text}")
48
+ break
49
+
50
+ print(json.loads(response.text))
51
+ return json.loads(response.text)
52
+
53
+ def wait_for_completion(self):
54
+ """Wait for the operation to complete"""
55
+ max_iter = 20
56
+ while self.state not in ('Succeeded', 'Failed'):
57
+ self.state = self.get_operation_state()["status"]
58
+ sleep(3)
59
+ if max_iter == 0:
60
+ raise Exception("Operation did not complete after 60 seconds")
61
+ max_iter -= 1
62
+ return self.state
63
+
64
+
65
+ def check_long_running_operation(headers, auth):
66
+ """Check the status of a long running operation"""
67
+ location = headers.get('Location', None)
68
+ operation_id = headers.get('x-ms-operation-id', None)
69
+ if location:
70
+ operation_id = location.split("/")[-1]
71
+
72
+ if not operation_id:
73
+ print("Operation initiated, no operation id found")
74
+ return None
75
+ else:
76
+ print("Operation initiated, waiting for completion")
77
+ lro = LongRunningOperation(operation_id=operation_id, auth=auth)
78
+ lro.wait_for_completion()
79
+ print("Operation completed")
80
+ lro = LongRunningOperation(operation_id, auth)
81
+ return lro.wait_for_completion()
@@ -0,0 +1,55 @@
1
+ import json
2
+ import requests
3
+ from time import sleep
4
+
5
+ class OneLakeShortcut:
6
+ """Class to represent a onelake shortcut in Microsoft Fabric"""
7
+
8
+ def __init__(self, name, path, workspace_id, item_id, target,
9
+ auth) -> None:
10
+
11
+ self.name = name
12
+ self.path = path
13
+ self.target = target
14
+ self.item_id = item_id
15
+ self.workspace_id = workspace_id
16
+
17
+ self.user_auth = auth
18
+
19
+ def __str__(self) -> str:
20
+ """Return a string representation of the workspace object"""
21
+ dict_ = {
22
+ 'name': self.name,
23
+ 'path': self.path,
24
+ 'target': self.target,
25
+ 'item_id': self.item_id,
26
+ 'workspace_id': self.workspace_id,
27
+ }
28
+ return json.dumps(dict_, indent=2)
29
+
30
+ def from_dict(short_dict, auth):
31
+ """Create OneLakeShortCut object from dictionary"""
32
+ return OneLakeShortcut(name=short_dict['name'],
33
+ path=short_dict['path'],
34
+ target=short_dict['target'],
35
+ item_id=short_dict['itemId'],
36
+ workspace_id=short_dict['workspaceId'],
37
+ auth=auth)
38
+
39
+ def delete(self):
40
+ """Delete the shortcut"""
41
+
42
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.workspace_id}/items/{self.item_id}/shortcuts/{self.path}/{self.name}"
43
+ for _ in range(10):
44
+ response = requests.delete(url=url, headers=self.user_auth.get_headers())
45
+ if response.status_code == 429:
46
+ print("Too many requests, waiting 10 seconds")
47
+ sleep(10)
48
+ continue
49
+ if response.status_code not in (200, 429):
50
+ print(response.status_code)
51
+ print(response.text)
52
+ raise Exception(f"Error deleting shortcut: {response.text}")
53
+ break
54
+
55
+ return response.status_code
@@ -0,0 +1,481 @@
1
+ import json
2
+ import requests
3
+ from time import sleep
4
+ from msfabricpysdkcore.item import Item
5
+ from msfabricpysdkcore.long_running_operation import check_long_running_operation
6
+
7
+ class Workspace:
8
+ """Class to represent a workspace in Microsoft Fabric"""
9
+
10
+ def __init__(self, id, display_name, description, type, auth, capacity_id = None) -> None:
11
+ self.id = id
12
+ self.display_name = display_name
13
+ self.description = description
14
+ self.type = type
15
+ self.capacity_id = capacity_id
16
+
17
+ self.auth = auth
18
+
19
+
20
+ def from_dict(dict, auth):
21
+ """Create a Workspace object from a dictionary"""
22
+ return Workspace(id=dict['id'], display_name=dict['displayName'], description=dict['description'], type=dict['type'], capacity_id=dict.get('capacityId', None),
23
+ auth=auth)
24
+
25
+ def __str__(self) -> str:
26
+ """Return a string representation of the workspace object"""
27
+ dict_ = {
28
+ 'id': self.id,
29
+ 'display_name': self.display_name,
30
+ 'description': self.description,
31
+ 'type': self.type,
32
+ 'capacity_id': self.capacity_id
33
+ }
34
+ return json.dumps(dict_, indent=2)
35
+
36
+ def get_role_assignments(self):
37
+ """Get role assignments for the workspace"""
38
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/roleAssignments"
39
+
40
+ for _ in range(10):
41
+ response = requests.get(url=url, headers=self.auth.get_headers())
42
+ if response.status_code == 429:
43
+ print("Too many requests, waiting 10 seconds")
44
+ sleep(10)
45
+ continue
46
+ if response.status_code not in (200, 429):
47
+ print(response.status_code)
48
+ print(response.text)
49
+ raise Exception(f"Error getting role assignments: {response.text}")
50
+ break
51
+
52
+ return json.loads(response.text)
53
+
54
+ def delete(self):
55
+ """Delete the workspace"""
56
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}"
57
+
58
+ for _ in range(10):
59
+ response = requests.delete(url=url, headers=self.auth.get_headers())
60
+ if response.status_code == 429:
61
+ print("Too many requests, waiting 10 seconds")
62
+ sleep(10)
63
+ continue
64
+ if response.status_code not in (200, 429):
65
+ print(response.status_code)
66
+ print(response.text)
67
+ raise Exception(f"Error deleting workspace: {response.text}")
68
+ break
69
+
70
+ return response.status_code
71
+
72
+ # function to add workpace role assignment
73
+ def add_role_assignment(self, role, principal):
74
+ """Add a role assignment to the workspace"""
75
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/roleAssignments"
76
+
77
+ payload = {
78
+ 'principal': principal,
79
+ 'role': role
80
+ }
81
+
82
+ for _ in range(10):
83
+ response = requests.post(url=url, headers=self.auth.get_headers(), data=json.dumps(payload))
84
+ if response.status_code == 429:
85
+ print("Too many requests, waiting 10 seconds")
86
+ sleep(10)
87
+ continue
88
+ if response.status_code not in (200, 429):
89
+ print(response.status_code)
90
+ print(response.text)
91
+ raise Exception(f"Error adding role assignments: {response.text}")
92
+ break
93
+
94
+ return response.status_code
95
+
96
+
97
+ def delete_role_assignment(self, principal_id):
98
+ """Delete a role assignment from the workspace"""
99
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/roleAssignments/{principal_id}"
100
+
101
+ for _ in range(10):
102
+ response = requests.delete(url=url, headers=self.auth.get_headers())
103
+ if response.status_code == 429:
104
+ print("Too many requests, waiting 10 seconds")
105
+ sleep(10)
106
+ continue
107
+ if response.status_code not in (200, 429):
108
+ print(response.status_code)
109
+ print(response.text)
110
+ raise Exception(f"Error deleting role assignments: {response.text}")
111
+ break
112
+
113
+
114
+ return response.status_code
115
+
116
+ def update(self, display_name = None, description = None):
117
+ """Update the workspace"""
118
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}"
119
+
120
+ body = dict()
121
+ if display_name:
122
+ body["displayName"] = display_name
123
+ if description:
124
+ body["description"] = description
125
+
126
+
127
+ for _ in range(10):
128
+ response = requests.patch(url=url, headers=self.auth.get_headers(), json=body)
129
+ if response.status_code == 429:
130
+ print("Too many requests, waiting 10 seconds")
131
+ sleep(10)
132
+ continue
133
+ if response.status_code not in (200, 429):
134
+ print(response.status_code)
135
+ print(response.text)
136
+ raise Exception(f"Error updating workspace: {response.text}")
137
+ break
138
+
139
+ assert response.status_code == 200
140
+ if display_name:
141
+ self.display_name = display_name
142
+ if description:
143
+ self.description = description
144
+
145
+ return self
146
+
147
+ def update_role_assignment(self, role, principal_id):
148
+ """Update a role assignment in the workspace"""
149
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/roleAssignments/{principal_id}"
150
+ body = {
151
+ 'role': role
152
+ }
153
+
154
+ for _ in range(10):
155
+ response = requests.patch(url=url, headers=self.auth.get_headers(), json=body)
156
+ if response.status_code == 429:
157
+ print("Too many requests, waiting 10 seconds")
158
+ sleep(10)
159
+ continue
160
+ if response.status_code not in (200, 429):
161
+ print(response.status_code)
162
+ print(response.text)
163
+ raise Exception(f"Error updating role assignments: {response.text}")
164
+ break
165
+
166
+ return response.status_code
167
+
168
+ def assign_to_capacity(self, capacity_id):
169
+ """Assign the workspace to a capacity"""
170
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/assignToCapacity"
171
+
172
+ body = {
173
+ 'capacityId': capacity_id
174
+ }
175
+
176
+
177
+ for _ in range(10):
178
+ response = requests.post(url=url, headers=self.auth.get_headers(), json=body)
179
+ if response.status_code == 429:
180
+ print("Too many requests, waiting 10 seconds")
181
+ sleep(10)
182
+ continue
183
+ if response.status_code == 202:
184
+ check_long_running_operation( response.headers, self.auth)
185
+ if response.status_code not in (202, 429):
186
+ print(response.status_code)
187
+ print(response.text)
188
+ raise Exception(f"Error assigning capacity: {response.text}")
189
+ break
190
+
191
+ assert response.status_code == 202
192
+ self.capacity_id = capacity_id
193
+ return response.status_code
194
+
195
+ def unassign_from_capacity(self):
196
+ """Unassign the workspace from a capacity"""
197
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/unassignFromCapacity"
198
+
199
+ for _ in range(10):
200
+ response = requests.post(url=url, headers=self.auth.get_headers())
201
+ if response.status_code == 429:
202
+ print("Too many requests, waiting 10 seconds")
203
+ sleep(10)
204
+ continue
205
+
206
+ if response.status_code == 202:
207
+ check_long_running_operation( response.headers, self.auth)
208
+ if response.status_code not in (202, 429):
209
+ print(response.status_code)
210
+ print(response.text)
211
+ raise Exception(f"Error unassigning capacity: {response.text}")
212
+ break
213
+
214
+ assert response.status_code == 202
215
+ self.capacity_id = None
216
+ return response.status_code
217
+
218
+ def create_item(self, display_name, type, definition = None, description = None):
219
+ """Create an item in a workspace"""
220
+
221
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/items"
222
+
223
+ body = {
224
+ 'displayName': display_name,
225
+ 'type': type,
226
+ 'definition': definition,
227
+ 'description': description
228
+ }
229
+
230
+ for _ in range(10):
231
+ response = requests.post(url=url, headers=self.auth.get_headers(), json=body)
232
+ if response.status_code == 429:
233
+ print("Too many requests, waiting 10 seconds")
234
+ sleep(10)
235
+ continue
236
+ if response.status_code == 202:
237
+ check_long_running_operation( response.headers, self.auth)
238
+ if response.status_code not in (201, 202, 429):
239
+ print(response.status_code)
240
+ print(response.text)
241
+ raise Exception(f"Error creating item: {response.text}")
242
+ break
243
+
244
+ item_dict = json.loads(response.text)
245
+ return Item.from_dict(item_dict, auth=self.auth)
246
+
247
+
248
+ def get_item(self, item_id):
249
+ # GET https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}/items/{itemId}
250
+ """Get an item from a workspace"""
251
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/items/{item_id}"
252
+
253
+ for _ in range(10):
254
+ response = requests.get(url=url, headers=self.auth.get_headers())
255
+ if response.status_code == 429:
256
+ print("Too many requests, waiting 10 seconds")
257
+ sleep(10)
258
+ continue
259
+ if response.status_code not in (200, 429):
260
+ print(response.status_code)
261
+ print(response.text)
262
+ raise Exception(f"Error getting item capacity: {response.text}")
263
+ break
264
+
265
+ item_dict = json.loads(response.text)
266
+ return Item.from_dict(item_dict, auth=self.auth)
267
+
268
+ def delete_item(self, item_id):
269
+ """Delete an item from a workspace"""
270
+ return self.get_item(item_id).delete()
271
+
272
+ def list_items(self):
273
+ """List items in a workspace"""
274
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/items"
275
+
276
+ for _ in range(10):
277
+ response = requests.get(url=url, headers=self.auth.get_headers())
278
+ if response.status_code == 429:
279
+ print("Too many requests, waiting 10 seconds")
280
+ sleep(10)
281
+ continue
282
+ if response.status_code not in (200, 429):
283
+ print(response.status_code)
284
+ print(response.text)
285
+ raise Exception(f"Error listing items: {response.text}")
286
+ break
287
+ items = json.loads(response.text)["value"]
288
+ return [Item.from_dict(item, auth=self.auth) for item in items]
289
+
290
+ def get_item_definition(self, item_id):
291
+ """Get the definition of an item from a workspace"""
292
+ return self.get_item(item_id).get_definition()
293
+
294
+ def update_item(self, item_id, display_name = None, description = None):
295
+ """Update an item in a workspace"""
296
+ return self.get_item(item_id=item_id).update(display_name=display_name, description=description)
297
+
298
+ def update_item_definition(self, item_id, definition):
299
+ """Update the definition of an item in a workspace"""
300
+ return self.get_item(item_id=item_id).update_definition(definition=definition)
301
+
302
+ def create_shortcut(self, item_id, path, name, target):
303
+ return self.get_item(item_id=item_id).create_shortcut(path=path, name=name, target=target)
304
+
305
+ def get_shortcut(self, item_id, path, name):
306
+ return self.get_item(item_id=item_id).get_shortcut(path=path, name=name)
307
+
308
+ def delete_shortcut(self, item_id, path, name):
309
+ return self.get_item(item_id=item_id).delete_shortcut(path=path, name=name)
310
+
311
+ def run_on_demand_item_job(self, item_id, job_type, execution_data = None):
312
+ return self.get_item(item_id=item_id).run_on_demand_job(job_type=job_type, execution_data = execution_data)
313
+
314
+ def get_item_job_instance(self, item_id, job_instance_id):
315
+ return self.get_item(item_id=item_id).get_item_job_instance(job_instance_id=job_instance_id)
316
+
317
+ def cancel_item_job_instance(self, item_id, job_instance_id):
318
+ return self.get_item(item_id=item_id).cancel_item_job_instance(job_instance_id=job_instance_id)
319
+
320
+ def git_connect(self, git_provider_details):
321
+ """Connect git"""
322
+ # POST https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}/git/connect
323
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/git/connect"
324
+
325
+ payload = {
326
+ 'gitProviderDetails': git_provider_details
327
+ }
328
+
329
+ for _ in range(10):
330
+ response = requests.post(url=url, headers=self.auth.get_headers(), json=payload)
331
+ if response.status_code == 429:
332
+ print("Too many requests, waiting 10 seconds")
333
+ sleep(10)
334
+ continue
335
+ if response.status_code not in (200, 204, 429):
336
+ print(response.status_code)
337
+ print(response.text)
338
+ raise Exception(f"Error connecting git: {response.text}")
339
+ break
340
+ return response.status_code
341
+
342
+ def git_disconnect(self):
343
+ """Disconnect git"""
344
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/git/disconnect"
345
+
346
+ for _ in range(10):
347
+ response = requests.post(url=url, headers=self.auth.get_headers())
348
+ if response.status_code == 429:
349
+ print("Too many requests, waiting 10 seconds")
350
+ sleep(10)
351
+ continue
352
+ if response.status_code not in (200, 204, 429):
353
+ print(response.status_code)
354
+ print(response.text)
355
+ raise Exception(f"Error disconnecting git: {response.text}")
356
+ break
357
+ return response.status_code
358
+
359
+ def git_initialize_connection(self, initialization_strategy):
360
+ # POST https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}/git/initializeConnection
361
+ """Initialize git connection"""
362
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/git/initializeConnection"
363
+
364
+ body = {'initializeGitConnectionRequest':initialization_strategy}
365
+
366
+ for _ in range(10):
367
+ response = requests.post(url=url, headers=self.auth.get_headers(), json=body)
368
+ if response.status_code == 429:
369
+ print("Too many requests, waiting 10 seconds")
370
+ sleep(10)
371
+ continue
372
+ if response.status_code == 202:
373
+ check_long_running_operation( response.headers, self.auth)
374
+ if response.status_code not in (200, 202, 429):
375
+ print(response.status_code)
376
+ print(response.text)
377
+ raise Exception(f"Error initializing connection: {response.text}")
378
+ break
379
+ return response.status_code
380
+
381
+ def git_get_status(self):
382
+ """Get git connection status"""
383
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/git/status"
384
+
385
+ for _ in range(10):
386
+ response = requests.get(url=url, headers=self.auth.get_headers())
387
+ if response.status_code == 429:
388
+ print("Too many requests, waiting 10 seconds")
389
+ sleep(10)
390
+ continue
391
+ if response.status_code == 202:
392
+ check_long_running_operation( response.headers, self.auth)
393
+ if response.status_code not in (200, 202, 429):
394
+ print(response.status_code)
395
+ print(response.text)
396
+ raise Exception(f"Error getting git connection status: {response.text}")
397
+ break
398
+ return json.loads(response.text)
399
+
400
+ def git_get_connection(self):
401
+ """Get git connection info"""
402
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/git/connection"
403
+
404
+ for _ in range(10):
405
+ response = requests.get(url=url, headers=self.auth.get_headers())
406
+ if response.status_code == 429:
407
+ print("Too many requests, waiting 10 seconds")
408
+ sleep(10)
409
+ continue
410
+ if response.status_code not in (200, 429):
411
+ print(response.status_code)
412
+ print(response.text)
413
+ raise Exception(f"Error getting git connection info: {response.text}")
414
+ break
415
+ return json.loads(response.text)
416
+
417
+ def commit_to_git(self, mode, comment=None, items=None, workspace_head=None):
418
+ # POST https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}/git/commitToGit
419
+
420
+ """Commit to git"""
421
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/git/commitToGit"
422
+
423
+ body = {
424
+ 'mode': mode
425
+ }
426
+
427
+ if comment:
428
+ body['comment'] = comment
429
+ if items:
430
+ body['items'] = items
431
+ if workspace_head:
432
+ body['workspaceHead'] = workspace_head
433
+
434
+ for _ in range(10):
435
+ response = requests.post(url=url, headers=self.auth.get_headers(), json=body)
436
+ if response.status_code == 429:
437
+ print("Too many requests, waiting 10 seconds")
438
+ sleep(10)
439
+ continue
440
+ if response.status_code == 202:
441
+ check_long_running_operation( response.headers, self.auth)
442
+ if response.status_code not in (200, 202, 429):
443
+ print(response.status_code)
444
+ print(response.text)
445
+ raise Exception(f"Error committing to git: {response.text}")
446
+ break
447
+
448
+ return response.status_code
449
+
450
+ def update_from_git(self, remote_commit_hash, conflict_resolution = None, options = None, workspace_head = None):
451
+ # POST https://api.fabric.microsoft.com/v1/workspaces/{workspaceId}/git/updateFromGit
452
+ """Update from git"""
453
+ url = f"https://api.fabric.microsoft.com/v1/workspaces/{self.id}/git/updateFromGit"
454
+
455
+ body = {
456
+ "remoteCommitHash" : remote_commit_hash
457
+ }
458
+
459
+ if conflict_resolution:
460
+ body['conflictResolution'] = conflict_resolution
461
+ if options:
462
+ body['options'] = options
463
+ if workspace_head:
464
+ body['workspaceHead'] = workspace_head
465
+
466
+ for _ in range(10):
467
+ response = requests.post(url=url, headers=self.auth.get_headers(), json=body)
468
+ if response.status_code == 429:
469
+ print("Too many requests, waiting 10 seconds")
470
+ sleep(10)
471
+ continue
472
+ if response.status_code == 202:
473
+ check_long_running_operation( response.headers, self.auth)
474
+
475
+ if response.status_code not in (200, 202, 429):
476
+ print(response.status_code)
477
+ print(response.text)
478
+ raise Exception(f"Error updating from git: {response.text}")
479
+ break
480
+
481
+ return response.status_code
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Andreas J. Rederer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE