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 +5 -0
- sai/__main__.py +4 -0
- sai/commands/__init__.py +0 -0
- sai/commands/auth_token.py +31 -0
- sai/commands/backend.py +22 -0
- sai/commands/checklib.py +146 -0
- sai/commands/ds/__init__.py +65 -0
- sai/commands/ds/create.py +138 -0
- sai/commands/ds/link.py +133 -0
- sai/commands/ds/pull.py +133 -0
- sai/commands/ds/rename.py +54 -0
- sai/commands/ds/send.py +161 -0
- sai/commands/ds/unlink.py +132 -0
- sai/commands/ds/utils.py +137 -0
- sai/commands/login.py +44 -0
- sai/commands/logout.py +37 -0
- sai/commands/me.py +48 -0
- sai/commands/open.py +1 -0
- sai/commands/pull.py +77 -0
- sai/commands/reload.py +51 -0
- sai/commands/sub.py +204 -0
- sai/commands/sync.py +687 -0
- sai/main.py +30 -0
- sai/state.py +92 -0
- sai/utils/client.py +49 -0
- sai/utils/error_handler.py +45 -0
- sprawdzai_cli-0.2.0.dist-info/METADATA +38 -0
- sprawdzai_cli-0.2.0.dist-info/RECORD +31 -0
- sprawdzai_cli-0.2.0.dist-info/WHEEL +5 -0
- sprawdzai_cli-0.2.0.dist-info/entry_points.txt +2 -0
- sprawdzai_cli-0.2.0.dist-info/top_level.txt +1 -0
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
sai/commands/__init__.py
ADDED
|
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'])
|
sai/commands/backend.py
ADDED
|
@@ -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)})")
|
sai/commands/checklib.py
ADDED
|
@@ -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()
|
sai/commands/ds/link.py
ADDED
|
@@ -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
|
+
|
sai/commands/ds/pull.py
ADDED
|
@@ -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()
|