sprawdzai-cli 0.2.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.
sai/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .state import set_current_backend, get_current_backend, resolve_backend
2
+ from .state import clear_token, clear_all_tokens
3
+ from .state import get_backend_url, get_frontend_url, normalize_url
4
+ from .commands.auth_token import get_auth_token
5
+ from .utils.client import APIClient
sai/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
File without changes
@@ -0,0 +1,31 @@
1
+ import typer
2
+ from ..state import resolve_backend, get_token
3
+ from typing import Optional
4
+ from ..utils.error_handler import write_error
5
+
6
+ def get_auth_token(backend: Optional[str]):
7
+ token = get_token(resolve_backend(backend))
8
+ if token is None:
9
+ raise Exception('You are not authenticated')
10
+ return token['token']
11
+
12
+ def auth_token(
13
+ backend: Optional[str] = typer.Option(
14
+ None,
15
+ "--backend",
16
+ "-b",
17
+ help=(
18
+ "Use a different backend: "
19
+ "local (l, l1, l2, ...), "
20
+ "remote (r, r1, r2, ...) "
21
+ "or full URL (https://example.com)"
22
+ )
23
+ )
24
+ ):
25
+ """
26
+ Show authentication token.
27
+ """
28
+ token = get_token(resolve_backend(backend))
29
+ if token is None:
30
+ write_error('You are not authenticated')
31
+ typer.echo(token['token'])
@@ -0,0 +1,22 @@
1
+ import typer
2
+ from ..state import set_current_backend, get_current_backend, get_backend_url
3
+ from typing import Optional
4
+
5
+ def backend(
6
+ backend: Optional[str] = typer.Argument(
7
+ None,
8
+ help=(
9
+ "Set backend: "
10
+ "local (l, l1, l2, ...), "
11
+ "remote (r, r1, r2, ...) "
12
+ "or full URL (https://example.com)"
13
+ )
14
+ )
15
+ ):
16
+ """
17
+ Show current globally used backend, or set a new backend if provided.
18
+ """
19
+ if backend is not None:
20
+ set_current_backend(backend)
21
+ backend = get_current_backend()
22
+ typer.echo(f"Using backend {backend} ({get_backend_url(backend)})")
@@ -0,0 +1,146 @@
1
+ import os
2
+ import sys
3
+ import json
4
+ import time
5
+ import traceback
6
+ import subprocess
7
+
8
+ RESULTS_SECRET_KEY: str = "secret_key"
9
+
10
+ # This flag is set to True when it is a validation submit, and False when it is a test set submit
11
+ IS_VALID: bool = True
12
+
13
+ # How long did notebook execution take (in seconds)
14
+ # Use this for raising Time Limit Exceeded exceptions when the total time of running notebook and pickle is too large
15
+ NOTEBOOK_EXECUTION_TIME: float = 0.0
16
+
17
+ # This is the same time limit that you provide on sprawdzai.org when editing the task, just for convenience (in seconds)
18
+ NOTEBOOK_TIME_LIMIT: int = 60
19
+
20
+ # The same as NOTEBOOK_TIME_LIMIT, but the checker limit
21
+ CHECKER_TIME_LIMIT: int = 60
22
+
23
+ def check_file_exists(filepath: str) -> bool:
24
+ if not os.path.exists(filepath):
25
+ return False
26
+ if not os.path.isfile(filepath):
27
+ return False
28
+ return True
29
+
30
+ def linear_grade(score: float, min_score: float, max_score: float, use_better: bool = False) -> int:
31
+ if min_score > max_score:
32
+ return linear_grade(max_score + min_score - score, max_score, min_score)
33
+ if score <= min_score:
34
+ return 0
35
+ if score >= max_score:
36
+ return 100
37
+ if use_better:
38
+ return int(round(99 * (score - min_score) / (max_score - min_score) + 0.5))
39
+ return int(round(100 * (score - min_score) / (max_score - min_score)))
40
+
41
+ def safe_score(func):
42
+ def wrapper(*args, **kwargs):
43
+ result = func(*args, **kwargs)
44
+
45
+ if isinstance(result, tuple):
46
+ score, notes = result
47
+
48
+ try:
49
+ score = int(score)
50
+ except Exception:
51
+ raise Exception(f"Exception in safe_score() wrapper: {func.__name__} returned non-numeric value: {score}")
52
+
53
+ return max(0, min(100, score)), notes
54
+ else:
55
+ try:
56
+ score = int(result)
57
+ except Exception:
58
+ raise Exception(f"Exception in safe_score() wrapper: {func.__name__} returned non-numeric value: {result}")
59
+
60
+ return max(0, min(100, score))
61
+
62
+ return wrapper
63
+
64
+ def debug(*values, sep=' ', end='\n'):
65
+ print(*values, sep=sep, end=end, file=sys.stderr, flush=True)
66
+
67
+ def format_time(t: float) -> str:
68
+ t = round(t)
69
+ if t < 0:
70
+ return f'-{format_time(-t)}'
71
+ if t < 3600:
72
+ return f'{t // 60}:{t % 60:02d}'
73
+ return f'{t // 3600}:{(t // 60) % 60:02d}:{t % 60:02d}'
74
+
75
+ def print_tree(startpath=".", prefix=""):
76
+ entries = sorted(os.listdir(startpath))
77
+ entries_count = len(entries)
78
+
79
+ for index, entry in enumerate(entries):
80
+ path = os.path.join(startpath, entry)
81
+ connector = "└─" if index == entries_count - 1 else "├─"
82
+ print(prefix + connector + entry)
83
+ if os.path.isdir(path):
84
+ extension = " " if index == entries_count - 1 else "│ "
85
+ print_tree(path, prefix + extension)
86
+
87
+ def run_pickle(timeout: float) -> float:
88
+ print('Executing pickle in isolated subprocess...')
89
+
90
+ start_time = time.time()
91
+
92
+ try:
93
+ proc = subprocess.run(
94
+ ['python', 'subprocess_script.py'],
95
+ timeout=timeout,
96
+ capture_output=True,
97
+ text=True
98
+ )
99
+ except subprocess.TimeoutExpired:
100
+ raise TimeError()
101
+
102
+ execution_time = time.time() - start_time
103
+
104
+ print('pickle subprocess return code:')
105
+ print(proc.returncode)
106
+ print('pickle subprocess stdout:')
107
+ print(proc.stdout)
108
+ print('pickle subprocess stderr:')
109
+ print(proc.stderr)
110
+
111
+ if check_file_exists('error.txt'):
112
+ try:
113
+ with open('error.txt', 'r', encoding='UTF-8') as f:
114
+ res = f.read()
115
+ except Exception:
116
+ traceback.print_exc()
117
+ raise SubmissionError()
118
+ raise SubmissionError(res)
119
+
120
+ return execution_time
121
+
122
+ # These are the contents of those functions on SprawdzAI:
123
+ def write_result_base(score: float = 0, error: str | None = None, notes: str = ""):
124
+ with open('result.txt', 'w', encoding='UTF-8') as f:
125
+ json.dump({
126
+ 'notes': notes,
127
+ 'error': error,
128
+ 'score': score,
129
+ 'secret_key': RESULTS_SECRET_KEY
130
+ }, f)
131
+ def write_ok(score: float, notes: str = ""):
132
+ write_result_base(score, None, notes)
133
+ def write_time_error(notes: str = ""):
134
+ write_result_base(0, "time_error", notes)
135
+ def write_memory_error(notes: str = ""):
136
+ write_result_base(0, "memory_error", notes)
137
+ def write_submission_error(notes: str = ""):
138
+ write_result_base(0, "runtime_error", notes)
139
+ def write_system_error(notes: str = ""):
140
+ write_result_base(0, "system_error", notes)
141
+
142
+ class SubmissionError(Exception):
143
+ pass
144
+
145
+ class TimeError(Exception):
146
+ pass
@@ -0,0 +1,65 @@
1
+ import typer
2
+ from typing import Optional
3
+ from pathlib import Path
4
+ from .pull import pull
5
+ from .send import send
6
+ from .create import create
7
+ from .link import link
8
+ from .unlink import unlink
9
+ from .rename import rename
10
+ from .utils import Datasets
11
+
12
+ ds_app = typer.Typer(
13
+ no_args_is_help=False,
14
+ help="Dataset management commands"
15
+ )
16
+
17
+ @ds_app.callback(invoke_without_command=True)
18
+ def ds(
19
+ ctx: typer.Context,
20
+ path: Path = typer.Option(
21
+ Path.cwd(),
22
+ "--location",
23
+ "-l",
24
+ help="Path where the task is located (defaults to current directory)."
25
+ ),
26
+ backend: Optional[str] = typer.Option(
27
+ None,
28
+ "--backend",
29
+ "-b",
30
+ help=(
31
+ "Use a different backend: "
32
+ "local (l, l1, l2, ...), "
33
+ "remote (r, r1, r2, ...) "
34
+ "or full URL (https://example.com)"
35
+ )
36
+ )
37
+ ):
38
+ if ctx.invoked_subcommand is None:
39
+ dss = Datasets(path, backend)
40
+
41
+ for test in [False, True]:
42
+ typer.secho(f'{"TEST" if test else "VALIDATION"} SUBMISSION DATASETS:', fg='blue')
43
+ for chk in [False, True]:
44
+ typer.secho(f' {"Checker" if chk else "Notebook"}:', fg='blue')
45
+ filtered = [ds for ds in dss.dss.values() if ds.stages['chk' if chk else 'nb']['test' if test else 'valid']]
46
+ if len(filtered) == 0:
47
+ typer.secho(' *no datasets*', fg='black')
48
+ else:
49
+ for ds in filtered:
50
+ typer.echo(' - ', nl=False)
51
+ if ds.local:
52
+ typer.secho(f'./{ds.name}', fg='green', nl=False)
53
+ else:
54
+ typer.secho(f'[MISSING] ./{ds.name}', fg='red', nl=False)
55
+ typer.echo(f' {ds.url}')
56
+ typer.echo('')
57
+
58
+ Datasets(path, backend).show_warnings()
59
+
60
+ ds_app.command()(pull)
61
+ ds_app.command()(send)
62
+ ds_app.command()(create)
63
+ ds_app.command()(link)
64
+ ds_app.command()(unlink)
65
+ ds_app.command()(rename)
@@ -0,0 +1,138 @@
1
+ import typer
2
+ from typing import Optional
3
+ from pathlib import Path
4
+ from .utils import Datasets, Dataset
5
+ from ...utils.error_handler import write_error, warn, handle_response_error
6
+ import tempfile
7
+ import zipfile
8
+
9
+ def create(
10
+ name: str = typer.Argument(
11
+ ...,
12
+ help=(
13
+ "Name of the dataset (name of the directory)"
14
+ )
15
+ ),
16
+ valid: bool = typer.Option(
17
+ False,
18
+ "--valid",
19
+ "-v",
20
+ ),
21
+ test: bool = typer.Option(
22
+ False,
23
+ "--test",
24
+ "-t",
25
+ ),
26
+ nb: bool = typer.Option(
27
+ False,
28
+ "--notebook",
29
+ "-n",
30
+ ),
31
+ chk: bool = typer.Option(
32
+ False,
33
+ "--checker",
34
+ "-c",
35
+ ),
36
+ path: Path = typer.Option(
37
+ Path.cwd(),
38
+ "--location",
39
+ "-l",
40
+ help="Path where the task is located (defaults to current directory)."
41
+ ),
42
+ backend: Optional[str] = typer.Option(
43
+ None,
44
+ "--backend",
45
+ "-b",
46
+ help=(
47
+ "Use a different backend: "
48
+ "local (l, l1, l2, ...), "
49
+ "remote (r, r1, r2, ...) "
50
+ "or full URL (https://example.com)"
51
+ )
52
+ )
53
+ ):
54
+ """
55
+ Create a new dataset with specified name.
56
+ """
57
+
58
+ dss = Datasets(path, backend)
59
+
60
+ if name in dss.dss:
61
+ write_error(f'A dataset with name ./{name} is already linked to this task. Please choose different name or rename the existing dataset with `sai ds rename {name} new-name`')
62
+
63
+ dir_path = path / name
64
+
65
+ if dir_path.exists():
66
+ if dir_path.is_file():
67
+ write_error(f'Specified name ./{name} is a file (expected it to not exist or be a directory).')
68
+ warn(f'The specified dataset folder ./{name} already exists, but its contents will not be sent.')
69
+ warn(f'Please run `sai ds send {name}` after this command to send its contents.')
70
+ else:
71
+ dir_path.mkdir()
72
+
73
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
74
+ tmp_path = Path(tmp.name)
75
+ try:
76
+ with zipfile.ZipFile(tmp_path, 'w', zipfile.ZIP_DEFLATED) as _:
77
+ pass
78
+ with open(tmp_path, "rb") as f:
79
+ files_req = {"file": (f'{name}.zip', f)}
80
+ data = {"type_": "dataset", "title": f"{dss.task['slug']} - {name}"}
81
+ resp = dss.client.post('/files', data=data, files=files_req)
82
+ finally:
83
+ tmp.close()
84
+ tmp_path.unlink()
85
+ if resp.status_code not in [200, 201]:
86
+ handle_response_error(resp)
87
+
88
+ res = resp.json()
89
+ res['type_'] = None
90
+ ds = Dataset(res, path)
91
+ dss.dss[ds.name] = ds
92
+ typer.secho(f'Created new dataset {str(ds)}', fg='green')
93
+
94
+
95
+ def linkk(s1: str, s2: str):
96
+ typer.secho(f'Linked to {s2} {"notebook" if s1 == "nb" else "checker"}!', fg='green')
97
+ ds.stages[s1][s2] = True
98
+
99
+ if nb and chk:
100
+ write_error('Expected one or none of flags --notebook and --checker, but got both.')
101
+ if valid and test:
102
+ write_error('Expected one or none of flags --valid and --test, but got both.')
103
+
104
+ flags = []
105
+ if nb:
106
+ flags.append('nb')
107
+ if chk:
108
+ flags.append('chk')
109
+ if valid:
110
+ flags.append('valid')
111
+ if test:
112
+ flags.append('test')
113
+
114
+ if len(flags) == 0:
115
+ linkk('nb', 'valid')
116
+ linkk('nb', 'test')
117
+ linkk('chk', 'valid')
118
+ linkk('chk', 'test')
119
+ elif len(flags) == 1:
120
+ if flags[0] in ['nb', 'chk']:
121
+ linkk(flags[0], 'valid')
122
+ linkk(flags[0], 'test')
123
+ else:
124
+ linkk('nb', flags[0])
125
+ linkk('chk', flags[0])
126
+ else:
127
+ if nb and valid:
128
+ linkk('nb', 'valid')
129
+ elif nb and test:
130
+ linkk('nb', 'test')
131
+ elif chk and valid:
132
+ linkk('chk', 'valid')
133
+ elif chk and test:
134
+ linkk('chk', 'test')
135
+
136
+ dss.update(dss.dss.values())
137
+
138
+ Datasets(path, backend).show_warnings()
@@ -0,0 +1,133 @@
1
+ import typer
2
+ from typing import Optional
3
+ from pathlib import Path
4
+ from .utils import Datasets, Dataset
5
+ from ...utils.error_handler import write_error, handle_response_error
6
+
7
+ def link(
8
+ name_or_id: str = typer.Argument(
9
+ ...,
10
+ help=(
11
+ "Name or id of dataset to be linked"
12
+ )
13
+ ),
14
+ valid: bool = typer.Option(
15
+ False,
16
+ "--valid",
17
+ "-v",
18
+ ),
19
+ test: bool = typer.Option(
20
+ False,
21
+ "--test",
22
+ "-t",
23
+ ),
24
+ nb: bool = typer.Option(
25
+ False,
26
+ "--notebook",
27
+ "-n",
28
+ ),
29
+ chk: bool = typer.Option(
30
+ False,
31
+ "--checker",
32
+ "-c",
33
+ ),
34
+ path: Path = typer.Option(
35
+ Path.cwd(),
36
+ "--location",
37
+ "-l",
38
+ help="Path where the task is located (defaults to current directory)."
39
+ ),
40
+ backend: Optional[str] = typer.Option(
41
+ None,
42
+ "--backend",
43
+ "-b",
44
+ help=(
45
+ "Use a different backend: "
46
+ "local (l, l1, l2, ...), "
47
+ "remote (r, r1, r2, ...) "
48
+ "or full URL (https://example.com)"
49
+ )
50
+ )
51
+ ):
52
+ """
53
+ Link a dataset to a stage of a task.
54
+
55
+ sai ds link name/id -> link dataset to all 4 stages
56
+ sai ds link name/id --notebook/--checker/--valid/--test or -n/-c/-v/-t -> link dataset to 2 corresponding stages
57
+
58
+ When passing two flags it will link dataset to this one stage, e.g.:
59
+ sai ds link name/id --notebook --test -> link dataset to test notebook stage
60
+ """
61
+
62
+ dss = Datasets(path, backend)
63
+
64
+ ds = dss.find(name_or_id, raise_not_found=False)
65
+
66
+ if ds is None:
67
+ files = []
68
+ for type_ in ['dataset', 'image', 'other']:
69
+ resp = dss.client.get(f'/files?type_={type_}')
70
+ if resp.status_code != 200:
71
+ handle_response_error(resp)
72
+ files += resp.json()
73
+
74
+ for f in files:
75
+ if f['id'] == name_or_id:
76
+ f['type_'] = 'none'
77
+ ds = Dataset(f, path)
78
+ typer.secho(f'Linking a new external dataset: {str(ds)}', fg='blue')
79
+ if ds.name in dss.dss:
80
+ write_error(f'A dataset with name "{ds.name}" is already linked to this task. Sai does not support two datasets having the same name. Please rename the currently linked dataset with `sai ds rename {ds.name} new-name` or change the name of external dataset manually on the website.')
81
+ dss.dss[ds.name] = ds
82
+
83
+ if ds is None:
84
+ write_error('Could not find dataset with specified name or id.')
85
+
86
+ def linkk(s1: str, s2: str):
87
+ if ds.stages[s1][s2]:
88
+ typer.secho(f'Already linked to {s2} {"notebook" if s1 == "nb" else "checker"}.', fg='yellow')
89
+ else:
90
+ typer.secho(f'Linked to {s2} {"notebook" if s1 == "nb" else "checker"}!', fg='green')
91
+ ds.stages[s1][s2] = True
92
+
93
+ if nb and chk:
94
+ write_error('Expected one or none of flags --notebook and --checker, but got both.')
95
+ if valid and test:
96
+ write_error('Expected one or none of flags --valid and --test, but got both.')
97
+
98
+ flags = []
99
+ if nb:
100
+ flags.append('nb')
101
+ if chk:
102
+ flags.append('chk')
103
+ if valid:
104
+ flags.append('valid')
105
+ if test:
106
+ flags.append('test')
107
+
108
+ if len(flags) == 0:
109
+ linkk('nb', 'valid')
110
+ linkk('nb', 'test')
111
+ linkk('chk', 'valid')
112
+ linkk('chk', 'test')
113
+ elif len(flags) == 1:
114
+ if flags[0] in ['nb', 'chk']:
115
+ linkk(flags[0], 'valid')
116
+ linkk(flags[0], 'test')
117
+ else:
118
+ linkk('nb', flags[0])
119
+ linkk('chk', flags[0])
120
+ else:
121
+ if nb and valid:
122
+ linkk('nb', 'valid')
123
+ elif nb and test:
124
+ linkk('nb', 'test')
125
+ elif chk and valid:
126
+ linkk('chk', 'valid')
127
+ elif chk and test:
128
+ linkk('chk', 'test')
129
+
130
+ dss.update(dss.dss.values())
131
+
132
+ Datasets(path, backend).show_warnings()
133
+
@@ -0,0 +1,133 @@
1
+ import typer
2
+ from typing import Optional
3
+ from pathlib import Path
4
+ from .utils import Datasets, Dataset
5
+ from ...utils.error_handler import warn, handle_response_error
6
+ import shutil
7
+ import tempfile
8
+ from tqdm.auto import tqdm
9
+ import zipfile
10
+
11
+ def pull(
12
+ name_or_id: Optional[str] = typer.Argument(
13
+ None,
14
+ help=(
15
+ "Pull a dataset with specific name or id. "
16
+ "If this argument is not specified, it will pull all missing datasets (ones that are linked to this task, but are not yet pulled locally)."
17
+ )
18
+ ),
19
+ only_missing: bool = typer.Option(
20
+ False,
21
+ "--missing",
22
+ "-m",
23
+ help=(
24
+ "Only pull missing datasets. "
25
+ "If this flag is not set, datasets that are already pulled locally will be pulled again in order to allow you to "
26
+ "update their contents in case they changed on backend "
27
+ "(but you will get warning before that, so you don't accidentally lose local changes)."
28
+ )
29
+ ),
30
+ path: Path = typer.Option(
31
+ Path.cwd(),
32
+ "--location",
33
+ "-l",
34
+ help="Path where the task is located (defaults to current directory)."
35
+ ),
36
+ backend: Optional[str] = typer.Option(
37
+ None,
38
+ "--backend",
39
+ "-b",
40
+ help=(
41
+ "Use a different backend: "
42
+ "local (l, l1, l2, ...), "
43
+ "remote (r, r1, r2, ...) "
44
+ "or full URL (https://example.com)"
45
+ )
46
+ )
47
+ ):
48
+ """
49
+ Pull a dataset or all missing datasets linked to a task.
50
+ """
51
+
52
+ dss = Datasets(path, backend)
53
+
54
+ tpull: list[Dataset] = []
55
+ if name_or_id is None:
56
+ if len(dss.dss) == 0:
57
+ warn('There are no datasets linked to this task, so no dataset can be pulled.')
58
+ else:
59
+ for ds in dss.dss.values():
60
+ if only_missing and ds.local:
61
+ continue
62
+ tpull.append(ds)
63
+ if len(tpull) == 0:
64
+ warn('You have set the --missing flag, but all datasets are already pulled locally.')
65
+ warn('If you think a dataset was changed on the remote and you want to redownload it, run `sai ds pull name/id` explicitly or remove the --missing flag.')
66
+ else:
67
+ ds = dss.find(name_or_id)
68
+ if only_missing and ds.local:
69
+ warn('You have set the --missing flag, but the dataset you picked is already pulled locally.')
70
+ else:
71
+ tpull.append(ds)
72
+
73
+ if len(tpull) >= 2:
74
+ typer.echo(f'Pulling {len(tpull)} datasets...')
75
+
76
+ for ds in tpull:
77
+ typer.echo(f'{" - " if len(tpull) >= 2 else ""}Pulling {str(ds)}...')
78
+
79
+ dir_path = path / ds.name
80
+
81
+ if ds.local:
82
+ res = typer.prompt(f'You are about to pull the "{ds.name}" dataset from backend. It will overwrite all your local data inside {dir_path} folder. Do you want to pull this dataset? [y]es/[n]o')
83
+ if type(res) != str or (res.lower() not in ['y', 'yes']):
84
+ warn("Dataset was skipped")
85
+ continue
86
+ shutil.rmtree(dir_path)
87
+
88
+ dir_path.mkdir(exist_ok=False)
89
+
90
+ is_zip = ds.filename.endswith('.zip')
91
+ if is_zip:
92
+ tmp = tempfile.NamedTemporaryFile(delete=False)
93
+ tmp_path = Path(tmp.name)
94
+ else:
95
+ tmp_path = dir_path / ds.filename
96
+ tmp = tmp_path.open("w+b")
97
+
98
+ url = f'/files/{ds.id}/download'
99
+ with dss.client.get(url, stream=True) as resp:
100
+ if resp.status_code != 200:
101
+ if tmp_path.exists():
102
+ tmp.close()
103
+ tmp_path.unlink()
104
+ typer.echo(f'Error while pulling {str(ds)} from {url}:')
105
+ handle_response_error(resp)
106
+
107
+ total_size = int(resp.headers.get("content-length", 0))
108
+
109
+ with tqdm(total=total_size, unit='B', unit_scale=True) as pbar:
110
+ for chunk in resp.iter_content(chunk_size=8192):
111
+ if chunk:
112
+ tmp.write(chunk)
113
+ pbar.update(len(chunk))
114
+
115
+ tmp.close()
116
+
117
+ if is_zip:
118
+ try:
119
+ with zipfile.ZipFile(tmp_path, 'r') as zip_ref:
120
+ zip_ref.extractall(dir_path)
121
+ typer.secho(f"Unzipped contents to {dir_path.absolute()}", fg=typer.colors.GREEN)
122
+ except zipfile.BadZipFile as e:
123
+ typer.echo(tmp_path.absolute())
124
+ typer.echo(e)
125
+ shutil.move(str(tmp_path), dir_path / (ds.name + '.zip'))
126
+ warn(f"Warning: downloaded {str(ds)} dataset is not a valid ZIP. It was placed in the folder instead of being unzipped.")
127
+ finally:
128
+ if tmp_path.exists():
129
+ tmp_path.unlink()
130
+ else:
131
+ typer.secho(f"Downloaded contents to {dir_path.absolute()}", fg=typer.colors.GREEN)
132
+
133
+ Datasets(path, backend).show_warnings()