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 +1 -0
- safe/assets/safe_logo.png +0 -0
- safe/main.py +324 -0
- safe/modules/__init__.py +1 -0
- safe/modules/export_module.py +65 -0
- safe/modules/import_module.py +71 -0
- safe/modules/list_export_module.py +61 -0
- safe/modules/select_helper.py +22 -0
- safe_file_example-0.1.0.dist-info/METADATA +135 -0
- safe_file_example-0.1.0.dist-info/RECORD +13 -0
- safe_file_example-0.1.0.dist-info/WHEEL +4 -0
- safe_file_example-0.1.0.dist-info/entry_points.txt +2 -0
- safe_file_example-0.1.0.dist-info/licenses/LICENSE +21 -0
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
|
+
|
safe/modules/__init__.py
ADDED
|
@@ -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,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.
|