safe-file-example 0.1.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.
safe/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.0"
Binary file
safe/main.py ADDED
@@ -0,0 +1,324 @@
1
+ import sys
2
+ import typer
3
+ import webbrowser
4
+ import jwt
5
+ import os
6
+ import time
7
+ import tkinter as tk
8
+ from tkinter import filedialog
9
+ from pathlib import Path
10
+ import safe.modules.import_module as import_module
11
+ import safe.modules.export_module as export_module
12
+ import safe.modules.list_export_module as list_export_module
13
+ import safe.modules.select_helper as select_option
14
+ import importlib.resources
15
+ try:
16
+ import readline
17
+ except ImportError:
18
+ pass
19
+
20
+ app = typer.Typer(invoke_without_command=True, add_completion=False)
21
+
22
+ # Variables
23
+ config_dir = os.path.expanduser("~/.safe-cli")
24
+ token_file = os.path.join(config_dir, "token.json")
25
+ base_url = "https://api.safe.uib.no/v1"
26
+ logo_path = importlib.resources.files("safe.assets").joinpath("safe_logo.png")
27
+
28
+ def get_access_token():
29
+ endpoint = "/auth"
30
+ auth_url = f"{base_url}{endpoint}"
31
+ webbrowser.open(auth_url)
32
+
33
+ Access_token = input("Paste You're JWT token here: ")
34
+
35
+ os.makedirs(config_dir, exist_ok=True)
36
+ with open(token_file, "w") as f:
37
+ f.write(Access_token)
38
+ if os.name != 'nt':
39
+ os.chmod(token_file, 0o600) #Change the ownership on Linux/MacOS
40
+ print("token succesfully stored")
41
+
42
+ def exit_cli():
43
+ sys.exit()
44
+
45
+ def importFile():
46
+ endpoint = "/files/import"
47
+ url = f"{base_url}{endpoint}"
48
+
49
+ # Read token from stored file
50
+ if not os.path.exists(token_file):
51
+ print("No token found. Run option 1 to authenticate.")
52
+ return
53
+
54
+ with open(token_file, "r") as f:
55
+ token = f.read().strip()
56
+
57
+ while True:
58
+ server = input("Enter the server name: ").strip()
59
+ if not server:
60
+ print("Please enter a valid server.")
61
+ continue
62
+ # File picker
63
+ print("opening file picker")
64
+ root = tk.Tk()
65
+ app_icon = tk.PhotoImage(file=str(logo_path))
66
+ root.withdraw()
67
+ root.attributes('-topmost', True)
68
+ root.iconphoto(True, app_icon)
69
+ root.update()
70
+ file_path_str = filedialog.askopenfilename(title="Select a file to import", initialdir=os.path.expanduser("~"))
71
+ root.attributes('-topmost', False)
72
+ root.iconphoto(False, app_icon)
73
+ root.destroy()
74
+
75
+ if not file_path_str:
76
+ print("No file selected, try again.")
77
+ continue
78
+
79
+ file_path = Path(file_path_str)
80
+
81
+ print("\n--- Import Summary ---")
82
+ print(f"\nServer: {server}")
83
+ print(f"File: {file_path.name}\n")
84
+
85
+ proceed = input("Proceed? (yes/no/change): ").strip().lower()
86
+
87
+ if proceed == "yes":
88
+ success = import_module.upload_file(
89
+ file_path=file_path,
90
+ url=url,
91
+ token=token,
92
+ filename_param=file_path.name,
93
+ server=server,
94
+ )
95
+ print(f"Import result: {success}")
96
+ break
97
+
98
+ elif proceed == "change":
99
+ continue
100
+ else:
101
+ print("Import cancelled.")
102
+ break
103
+
104
+ def exportFile():
105
+ export_endpoint = "/files/export"
106
+ export_url = f"{base_url}{export_endpoint}"
107
+
108
+ # Read token
109
+ if not os.path.exists(token_file):
110
+ print("No token found. Run option 1 to authenticate.")
111
+ return
112
+
113
+ with open(token_file, "r") as f:
114
+ token = f.read().strip()
115
+
116
+ while True:
117
+ server = input("Enter the server name: ").strip()
118
+ if not server:
119
+ print("Please enter a valid server.")
120
+ continue
121
+
122
+ # ---- LIST AVAILABLE FILES ----
123
+ files = list_export_module.list_export_files(
124
+ base_url=base_url,
125
+ token=token,
126
+ server=server,
127
+ )
128
+
129
+ if not files:
130
+ print("No exportable files found.")
131
+ return
132
+
133
+ while True:
134
+ filename = select_option.select_from_list(
135
+ files,
136
+ "Select file by number or type filename: "
137
+ )
138
+ if filename:
139
+ break
140
+
141
+
142
+ # ---- Directory picker ----
143
+ print("Select output directory")
144
+ root = tk.Tk()
145
+ app_icon = tk.PhotoImage(file=str(logo_path))
146
+ root.withdraw()
147
+ root.attributes("-topmost", True)
148
+ root.iconphoto(True, app_icon)
149
+ root.update()
150
+ output_dir = filedialog.askdirectory(title="Select output directory", initialdir=os.path.expanduser("~"))
151
+ root.attributes("-topmost", False)
152
+ root.iconphoto(False, app_icon)
153
+ root.destroy()
154
+ # ---------------------------
155
+
156
+ if not output_dir:
157
+ print("No directory selected, try again.")
158
+ continue
159
+
160
+ output_path = Path(output_dir) / filename
161
+
162
+ print("\n--- Export Summary ---")
163
+ print(f"Server: {server}")
164
+ print(f"Filename: {filename}")
165
+ print(f"Output: {output_path}")
166
+ print("----------------------\n")
167
+
168
+ proceed = input("Proceed? (yes/no/change): ").strip().lower()
169
+
170
+ if proceed == "yes":
171
+ success = export_module.write_file_to_disk(
172
+ download_path=output_path,
173
+ download_url=export_url,
174
+ token=token,
175
+ server=server,
176
+ filename=filename,
177
+ )
178
+ print(f"Export result: {success}")
179
+ return
180
+
181
+ elif proceed == "change":
182
+ continue
183
+
184
+ else:
185
+ print("Export cancelled.")
186
+ return
187
+
188
+ def check_token():
189
+ if os.path.exists(token_file):
190
+ with open(token_file, "r") as f:
191
+ token = f.read().strip()
192
+ try:
193
+ decoded = jwt.decode(token, options={"verify_signature": False})
194
+ exp = decoded.get("exp")
195
+ if exp is not None:
196
+ current_time = int(time.time())
197
+ if current_time > exp:
198
+ print("The stored JWT token is expired.")
199
+ else:
200
+ print("The stored JWT token is valid.")
201
+ else:
202
+ print("No expiration (exp) claim found in the JWT token.")
203
+ except Exception as e:
204
+ print(f"Failed to decode JWT token: {e}")
205
+ else:
206
+ print("No JWT token found in the token file.")
207
+
208
+ @app.callback()
209
+ def main(ctx:typer.Context):
210
+ if ctx.invoked_subcommand is None:
211
+ typer.echo(ctx.get_help())
212
+ raise typer.Exit()
213
+
214
+ @app.command()
215
+ def token():
216
+ """Get and store access token"""
217
+ get_access_token()
218
+
219
+ @app.command()
220
+ def import_file(
221
+ server: str = typer.Option(None, help="Server name"),
222
+ file_path: Path = typer.Option(None, exists=True, file_okay=True, dir_okay=False, readable=True, help="Path to file to import"),
223
+ interactive: bool = typer.Option(False, "-i", "--interactive", help="Run import interactively"),
224
+ ):
225
+ """Import a file"""
226
+ if interactive or not (server and file_path):
227
+ # Run interactive import
228
+ importFile()
229
+ else:
230
+ # Non-interactive import
231
+ endpoint = "/files/import"
232
+ url = f"{base_url}{endpoint}"
233
+
234
+ if not os.path.exists(token_file):
235
+ typer.echo("No token found. Run 'get-token' to authenticate.")
236
+ raise typer.Exit(code=1)
237
+
238
+ with open(token_file, "r") as f:
239
+ token = f.read().strip()
240
+
241
+ typer.echo(f"Importing file {file_path} to server {server}...")
242
+
243
+ success = import_module.upload_file(
244
+ file_path=file_path,
245
+ url=url,
246
+ token=token,
247
+ filename_param=file_path.name,
248
+ server=server,
249
+ )
250
+ typer.echo(f"Import result: {success}")
251
+
252
+ @app.command()
253
+ def export_file(
254
+ server: str = typer.Option(None, help="Server name"),
255
+ filename: str = typer.Option(None, help="Filename to export"),
256
+ output_dir: Path = typer.Option(None, exists=True, file_okay=False, dir_okay=True, writable=True, help="Output directory"),
257
+ interactive: bool = typer.Option(False, "-i", "--interactive", help="Run export interactively"),
258
+ ):
259
+ """Export a file"""
260
+ if interactive or not (server and filename and output_dir):
261
+ # Run interactive export
262
+ exportFile()
263
+ else:
264
+ # Non-interactive export
265
+ export_endpoint = "/files/export"
266
+ export_url = f"{base_url}{export_endpoint}"
267
+
268
+ if not os.path.exists(token_file):
269
+ typer.echo("No token found. Run 'get-token' to authenticate.")
270
+ raise typer.Exit(code=1)
271
+
272
+ with open(token_file, "r") as f:
273
+ token = f.read().strip()
274
+
275
+ output_path = output_dir / filename
276
+
277
+ typer.echo(f"Exporting file {filename} from server {server} to {output_path}...")
278
+
279
+ success = export_module.write_file_to_disk(
280
+ download_path=output_path,
281
+ download_url=export_url,
282
+ token=token,
283
+ server=server,
284
+ filename=filename,
285
+ )
286
+ typer.echo(f"Export result: {success}")
287
+
288
+
289
+ def interactive_menu():
290
+ check_token()
291
+ choices: list = [get_access_token, importFile, exportFile, exit_cli]
292
+ while True:
293
+ print("\n--- SAFE CLI ---")
294
+ for index, item in enumerate(choices, start=1):
295
+ print(f"{index}. {item.__name__}")
296
+
297
+ menu_names = [fn.__name__ for fn in choices]
298
+ selection = select_option.select_from_list(
299
+ menu_names,
300
+ "Enter menu number or command name: "
301
+ )
302
+ if not selection:
303
+ continue
304
+
305
+ selected_fn = choices[menu_names.index(selection)]
306
+
307
+ if selected_fn == exit_cli:
308
+ exit_cli()
309
+ return
310
+ else:
311
+ selected_fn()
312
+
313
+ @app.command(name="interactive")
314
+ def interactive_command():
315
+ """Run interactive mode"""
316
+ interactive_menu()
317
+
318
+ app.command()(token)
319
+ app.command()(import_file)
320
+ app.command()(export_file)
321
+
322
+ if __name__ == "__main__":
323
+ app()
324
+
@@ -0,0 +1 @@
1
+ __version__ = "0.0.0"
@@ -0,0 +1,65 @@
1
+ from pathlib import Path
2
+ import os
3
+ import shutil
4
+ import requests
5
+
6
+
7
+ def write_file_to_disk(
8
+ download_path: Path,
9
+ download_url: str,
10
+ token: str,
11
+ server: str,
12
+ filename: str,
13
+ chunk_size: int = 8 * 1024 * 1024,
14
+ connect_timeout: float = 15,
15
+ read_timeout: float = 600,
16
+ ) -> bool:
17
+ print(f"Starting download to {download_path}")
18
+
19
+ temp_path = download_path.with_suffix(download_path.suffix + ".safetmp")
20
+ download_path.parent.mkdir(parents=True, exist_ok=True)
21
+
22
+ headers = {
23
+ "Authorization": f"Bearer {token}"
24
+ }
25
+
26
+ params = {
27
+ "filename": filename,
28
+ "server": server,
29
+ }
30
+
31
+ try:
32
+ with requests.get(
33
+ download_url,
34
+ headers=headers,
35
+ params=params,
36
+ stream=True,
37
+ timeout=(connect_timeout, read_timeout),
38
+ ) as response:
39
+ response.raise_for_status()
40
+
41
+ total = int(response.headers.get("Content-Length") or 0)
42
+ if total:
43
+ free = shutil.disk_usage(download_path.parent).free
44
+ if total > free:
45
+ raise OSError(
46
+ f"Not enough free disk space ({free} bytes) for {total} bytes"
47
+ )
48
+
49
+ with open(temp_path, "wb") as f:
50
+ for chunk in response.iter_content(chunk_size=chunk_size):
51
+ if chunk:
52
+ f.write(chunk)
53
+
54
+ os.replace(temp_path, download_path)
55
+ print("download completed:", download_path)
56
+ return True
57
+
58
+ except Exception as e:
59
+ if temp_path.exists():
60
+ try:
61
+ temp_path.unlink()
62
+ except Exception:
63
+ pass
64
+ print("Download failed")
65
+ return False
@@ -0,0 +1,71 @@
1
+ import os
2
+ import mimetypes
3
+ import requests
4
+ from pathlib import Path
5
+
6
+
7
+ def open_file_stream(path: Path):
8
+ return path.open("rb")
9
+
10
+
11
+ def upload_file(
12
+ file_path: Path,
13
+ url: str,
14
+ token: str,
15
+ filename_param: str,
16
+ server: str,
17
+ ) -> bool:
18
+ if not file_path.is_file():
19
+ raise FileNotFoundError(f"File not found: {file_path}")
20
+ if not server:
21
+ raise RuntimeError("Server name not provided")
22
+
23
+ mime, _ = mimetypes.guess_type(file_path.name)
24
+ content_type = mime or "application/octet-stream"
25
+
26
+ headers = {
27
+ "Authorization": f"Bearer {token}",
28
+ "Content-Type": content_type,
29
+ }
30
+ params = {"filename": filename_param, "server": server}
31
+
32
+ print(f"Uploading {file_path.name} ({content_type})...")
33
+
34
+ try:
35
+ with open_file_stream(file_path) as f:
36
+ resp = requests.post(
37
+ url,
38
+ headers=headers,
39
+ params=params,
40
+ data=f,
41
+ )
42
+
43
+ # Error handling
44
+ if resp.status_code == 400:
45
+ print("Bad Request - invalid filename or no file.")
46
+ print(resp.text)
47
+ return False
48
+ if resp.status_code == 401:
49
+ print("Unauthorized - no import access.")
50
+ print(resp.text)
51
+ return False
52
+ if resp.status_code == 409:
53
+ print("Conflict - file already exists.")
54
+ print(resp.text)
55
+ return False
56
+ if resp.status_code == 500:
57
+ print("Internal server error.")
58
+ print(resp.text)
59
+ return False
60
+
61
+ resp.raise_for_status()
62
+ print("Upload succeeded:", resp.text)
63
+ return True
64
+
65
+ except requests.HTTPError as http_err:
66
+ print("Unexpected HTTP error:", http_err)
67
+ print("Response:", http_err.response.text)
68
+ return False
69
+ except Exception as e:
70
+ print("Unexpected error:", type(e).__name__, e)
71
+ return False
@@ -0,0 +1,61 @@
1
+ import requests
2
+ from typing import List
3
+
4
+ def list_export_files(
5
+ base_url: str,
6
+ token: str,
7
+ server: str,
8
+ page: int = 1,
9
+ pagination: bool = True,
10
+ timeout: int = 30,
11
+ ) -> List[str]:
12
+
13
+ url = f"{base_url}/files/info/list/export"
14
+
15
+ headers = {
16
+ "Authorization": f"Bearer {token}"
17
+ }
18
+
19
+ params = {
20
+ "server": server,
21
+ "page": page,
22
+ "pagination": pagination,
23
+ }
24
+
25
+ try:
26
+ resp = requests.get(
27
+ url,
28
+ headers=headers,
29
+ params=params,
30
+ timeout=timeout,
31
+ )
32
+ resp.raise_for_status()
33
+
34
+ data = resp.json()
35
+
36
+ files = []
37
+
38
+ # Extract filenames
39
+ content = data.get("content", {})
40
+ entries = content.get("Files and Directories", [])
41
+ for entry in entries:
42
+ if isinstance(entry, dict):
43
+ files.extend(entry.keys())
44
+
45
+ # Pretty print
46
+ print("\n--- Available export files ---")
47
+ if not files:
48
+ print("(No files found)")
49
+ else:
50
+ for i, name in enumerate((files), start=1):
51
+ print(f"{i}. {name}")
52
+ print("------------------------------\n")
53
+
54
+ return files
55
+
56
+ except requests.HTTPError as e:
57
+ print(f"HTTP {e.response.status_code}: {e.response.text}")
58
+ return []
59
+ except Exception as e:
60
+ print(f"Unexpected error: {e}")
61
+ return []
@@ -0,0 +1,22 @@
1
+
2
+ def select_from_list(options: list[str], prompt: str) -> str | None:
3
+ """
4
+ Allow selection by number or exact name.
5
+ Returns the selected string or None if invalid.
6
+ """
7
+ choice = input(prompt).strip()
8
+
9
+ # Number selection
10
+ if choice.isdigit():
11
+ idx = int(choice) - 1
12
+ if 0 <= idx < len(options):
13
+ return options[idx]
14
+ print("Invalid number.")
15
+ return None
16
+
17
+ # Name selection
18
+ if choice in options:
19
+ return choice
20
+
21
+ print("Invalid selection.")
22
+ return None
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: safe-file-example
3
+ Version: 0.1.0
4
+ Summary: A python package for interacting with SAFE File transfer API
5
+ Project-URL: Homepage, https://itgit.app.uib.no/it/dpv/df/sfl/safe/development-group/experiments/safe-cli
6
+ Author-email: Dip Paul <dip.paul@uib.no>, Eirik Halseth <eirik.halseth@uib.no>, Marco Antonio Corrales Cardenas <marco.corrales@uib.no>
7
+ License-File: LICENSE
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: annotated-doc==0.0.4
12
+ Requires-Dist: certifi==2026.2.25
13
+ Requires-Dist: charset-normalizer==3.4.7
14
+ Requires-Dist: click==8.3.2
15
+ Requires-Dist: idna==3.11
16
+ Requires-Dist: markdown-it-py==4.0.0
17
+ Requires-Dist: mdurl==0.1.2
18
+ Requires-Dist: pygments==2.20.0
19
+ Requires-Dist: pyjwt==2.12.1
20
+ Requires-Dist: requests==2.33.1
21
+ Requires-Dist: rich==15.0.0
22
+ Requires-Dist: shellingham==1.5.4
23
+ Requires-Dist: typer==0.24.1
24
+ Requires-Dist: urllib3==2.6.3
25
+ Description-Content-Type: text/markdown
26
+
27
+ # `safe-file-example`
28
+
29
+ ## Description
30
+
31
+ A python package for interacting with SAFE File transfer API
32
+
33
+ ## Current Functionalities
34
+
35
+ - Generate a JWT Token
36
+ - Import a File
37
+ - Export a File
38
+
39
+ ## Prerequisite
40
+
41
+ - Having minimum Python 3.12 installed in your system and setting up a Python Virtual Environment.
42
+
43
+ ## Workflow
44
+
45
+ - Activate your Python Virtual Environment,
46
+ - Then run the following command.
47
+
48
+ ```bash
49
+ pip install safe-file-example
50
+ ```
51
+ - To see all the available commands and options run the following
52
+
53
+ ```bash
54
+ safe-file-example
55
+ ```
56
+
57
+ **Usage**:
58
+
59
+ ```console
60
+ $ safe-file-example [OPTIONS] COMMAND [ARGS]...
61
+ ```
62
+
63
+ **Options**:
64
+
65
+ * `--help`: Show this message and exit.
66
+
67
+ **Commands**:
68
+
69
+ * `token`: Get and store access token
70
+ * `import-file`: Import a file
71
+ * `export-file`: Export a file
72
+ * `interactive`: Run interactive mode
73
+
74
+ ## `safe-file-example token`
75
+
76
+ Get and store access token
77
+
78
+ **Usage**:
79
+
80
+ ```console
81
+ $ safe-file-example token [OPTIONS]
82
+ ```
83
+
84
+ **Options**:
85
+
86
+ * `--help`: Show this message and exit.
87
+
88
+ ## `safe-file-example import-file`
89
+
90
+ Import a file
91
+
92
+ **Usage**:
93
+
94
+ ```console
95
+ $ safe-file-example import-file [OPTIONS]
96
+ ```
97
+
98
+ **Options**:
99
+
100
+ * `--server TEXT`: Server name
101
+ * `--file-path FILE`: Path to file to import
102
+ * `-i, --interactive`: Run import interactively
103
+ * `--help`: Show this message and exit.
104
+
105
+ ## `safe-file-example export-file`
106
+
107
+ Export a file
108
+
109
+ **Usage**:
110
+
111
+ ```console
112
+ $ safe-file-example export-file [OPTIONS]
113
+ ```
114
+
115
+ **Options**:
116
+
117
+ * `--server TEXT`: Server name
118
+ * `--filename TEXT`: Filename to export
119
+ * `--output-dir DIRECTORY`: Output directory
120
+ * `-i, --interactive`: Run export interactively
121
+ * `--help`: Show this message and exit.
122
+
123
+ ## `safe-file-example interactive`
124
+
125
+ Run interactive mode
126
+
127
+ **Usage**:
128
+
129
+ ```console
130
+ $ safe-file-example interactive [OPTIONS]
131
+ ```
132
+
133
+ **Options**:
134
+
135
+ * `--help`: Show this message and exit.
@@ -0,0 +1,13 @@
1
+ safe/__init__.py,sha256=qwX_VmtTcxitUuo61-lzyWhU70ydr0gDejMn5eqC3Dk,21
2
+ safe/main.py,sha256=tK3n09FSwRFr_g6pQ3ScYPsKZ7YtHuSjwMNgok8QIl4,9765
3
+ safe/assets/safe_logo.png,sha256=B4jKkV7eCUnpN8RG3-WgfBfq8XZSgfH8BBpQ_NeUsQ4,5194
4
+ safe/modules/__init__.py,sha256=qwX_VmtTcxitUuo61-lzyWhU70ydr0gDejMn5eqC3Dk,21
5
+ safe/modules/export_module.py,sha256=D5Eltciypvdxg89TIno-mgBmCtnMECa4iz685H9kKgc,1758
6
+ safe/modules/import_module.py,sha256=42maaFQhC9gl9dyax0lSn2u1pVBbbotfciNq2XlMXY8,1940
7
+ safe/modules/list_export_module.py,sha256=L3iI9KsTAo7NLDjuooMPMSZ09fNd2AnS3DJUsAtoMJQ,1416
8
+ safe/modules/select_helper.py,sha256=kWlQVHTMzeI7kjqH5RPQcYGQKXMQt4Jw83e3TUyo9Is,537
9
+ safe_file_example-0.1.0.dist-info/METADATA,sha256=i2wQmcm9pPmnvZR59mOFIEM1sKV9PMqgBHkPd_VXOaI,2820
10
+ safe_file_example-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ safe_file_example-0.1.0.dist-info/entry_points.txt,sha256=coCH6K8wmaoh76SFtz3WZDJmX6MFXRUFlScszm7qYNk,52
12
+ safe_file_example-0.1.0.dist-info/licenses/LICENSE,sha256=q-Uz9BRmoLloIbL-0Ql3YvcvW6TdzzLrJUxeOtmSAjI,1077
13
+ safe_file_example-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ safe-file-example = safe.main:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 University of Bergen
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.