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/commands/pull.py ADDED
@@ -0,0 +1,77 @@
1
+ import typer
2
+ from ..state import resolve_backend
3
+ from typing import Optional
4
+ from ..utils.error_handler import handle_response_error, write_error
5
+ from ..utils.client import APIClient
6
+ from .sync import sync_task, write_dotsai
7
+ from pathlib import Path
8
+ from .ds.pull import pull as pull_datasets
9
+
10
+ def pull(
11
+ task: str = typer.Argument(
12
+ ...,
13
+ help="id / slug / url"
14
+ ),
15
+ path: Optional[Path] = typer.Option(
16
+ None,
17
+ "--location",
18
+ "-l",
19
+ help="Path where the task should be pulled."
20
+ ),
21
+ backend: Optional[str] = typer.Option(
22
+ None,
23
+ "--backend",
24
+ "-b",
25
+ help=(
26
+ "Use a different backend: "
27
+ "local (l, l1, l2, ...), "
28
+ "remote (r, r1, r2, ...) "
29
+ "or full URL (https://example.com)"
30
+ )
31
+ )
32
+ ):
33
+ """
34
+ Pull a task from server.
35
+ """
36
+
37
+ backend = resolve_backend(backend)
38
+ client = APIClient(backend)
39
+
40
+ if '/' in task:
41
+ spt = task.split('/')
42
+ task_slug = None
43
+ for i in range(len(spt) - 1):
44
+ if spt[i] == 'zadanie':
45
+ task_slug = spt[i + 1]
46
+ break
47
+ if task_slug is None:
48
+ write_error('Could not decode task slug from the url')
49
+ else:
50
+ task = task_slug
51
+
52
+ typer.echo(f'Pulling task {task} from {client.base_url}/tasks/{task}\n')
53
+
54
+ task_resp = client.get(f'/tasks/{task}')
55
+ if task_resp.status_code != 200:
56
+ handle_response_error(task_resp)
57
+
58
+ res = task_resp.json()
59
+
60
+ if path == None:
61
+ path = Path.cwd() / str(res['slug'])
62
+
63
+ try:
64
+ path.mkdir(exist_ok=False)
65
+ except:
66
+ write_error(f'Directory {path.absolute()} already exists.')
67
+
68
+ write_dotsai(path, {'id': res['id']})
69
+ sync_task(
70
+ path,
71
+ client,
72
+ force_remote=True,
73
+ simulate=False
74
+ )
75
+ pull_datasets(None, False, path, backend)
76
+
77
+ typer.secho(f'Pulled task into {path.absolute()}', fg=typer.colors.GREEN)
sai/commands/reload.py ADDED
@@ -0,0 +1,51 @@
1
+ import typer
2
+ from ..state import resolve_backend
3
+ from .sync import read_dotsai, write_dotsai
4
+ from typing import Optional
5
+ from pathlib import Path
6
+
7
+ def reload(
8
+ force: bool = typer.Option(
9
+ False,
10
+ "--force",
11
+ help="Reload all files (all local changes will be lost)"
12
+ ),
13
+ path: Path = typer.Option(
14
+ Path.cwd(),
15
+ "--location",
16
+ "-l",
17
+ help="Path where the task is located (defaults to current directory)."
18
+ ),
19
+ backend: Optional[str] = typer.Option(
20
+ None,
21
+ "--backend",
22
+ "-b",
23
+ help=(
24
+ "Used only when --force is used. "
25
+ "Use a different backend: "
26
+ "local (l, l1, l2, ...), "
27
+ "remote (r, r1, r2, ...) "
28
+ "or full URL (https://example.com)"
29
+ )
30
+ )
31
+ ):
32
+ """
33
+ Reload current task in case of version incompatibility errors.
34
+
35
+ By default it resets the .sai file.
36
+ This way all local changes are preserved, but it will lose track of any conflicts until next sync.
37
+ After running reset, any changes that have been made on backend (since last sync) will be automatically
38
+ overwritten by local changes when running next sync.
39
+
40
+ When using --force, it will also fetch all files in the task from server.
41
+ It is the same as deleting the whole task directory and pulling it again
42
+ """
43
+
44
+ dotsai = read_dotsai(path)
45
+ if not force:
46
+ write_dotsai(path, {'id': dotsai['id']})
47
+ return
48
+
49
+ backend = resolve_backend(backend)
50
+ raise NotImplementedError()
51
+
sai/commands/sub.py ADDED
@@ -0,0 +1,204 @@
1
+ import typer
2
+ from ..state import resolve_backend, get_frontend_url
3
+ from typing import Optional
4
+ from ..utils.error_handler import handle_response_error, write_error
5
+ from ..utils.client import APIClient
6
+ from .sync import read_dotsai, read_metadata
7
+ from pathlib import Path
8
+ import os
9
+ import webbrowser
10
+
11
+ def norm_filename(n: str):
12
+ return (n[1:] if n[0] == '_' else n).lower()
13
+
14
+ def supath(current_path: Path, path: str, safe: bool):
15
+ spt = path.split('/')
16
+ for idx, el in enumerate(spt):
17
+ if el == '.':
18
+ continue
19
+ if el == '..':
20
+ current_path = current_path.parent
21
+ continue
22
+ try:
23
+ filenames = os.listdir(current_path)
24
+ except NotADirectoryError:
25
+ write_error(f'{current_path} is not a directory')
26
+ continue
27
+ if el in filenames:
28
+ current_path = current_path / el
29
+ continue
30
+ if idx == len(spt) - 1:
31
+ filenames = [n for n in filenames if (current_path / n).is_file()]
32
+ else:
33
+ filenames = [n for n in filenames if (current_path / n).is_dir()]
34
+ filenames = [n for n in filenames if norm_filename(n).startswith(el)]
35
+ if len(filenames) == 0:
36
+ write_error(f'Could not find a {"file" if len(spt) - 1 else "folder"} that starts with {el} inside folder {current_path}')
37
+ if len(filenames) > 1 and safe:
38
+ write_error(f'Found more than one option to complete {el} inside folder {current_path}, for example {filenames[0]} and {filenames[1]}')
39
+ min_idx = 0
40
+ for i in range(1, len(filenames)):
41
+ if norm_filename(filenames[i]) < norm_filename(filenames[min_idx]):
42
+ min_idx = i
43
+ current_path = current_path / filenames[min_idx]
44
+
45
+ return current_path
46
+
47
+ def open_in_browser(link: str):
48
+ webbrowser.open_new_tab(link)
49
+
50
+ def sub(
51
+ paths: list[str] = typer.Argument(
52
+ help="""
53
+ Paths to submission files, in the order as they appear in submission_files metadata or on the website.
54
+
55
+ You can type only the first few letters of the filename or any other part of path to the submission,
56
+ and it will automatically take the lexicographically first completion of the path.
57
+ When searching for such files, it ignores a single `_` character at the beginning of the filename if present.
58
+ It also converts all filenames to lowercase before searching.
59
+
60
+ For example, to submit `1-train/_submission.ipynb`, you may just type `sai sub 1/s`.
61
+
62
+ This is made in order to let you quickly send submissions inside training folder (so they get the same environment as on the platform)
63
+ or to hold the submissions in a separate directory.
64
+ """
65
+ ),
66
+ valid: bool = typer.Option(
67
+ False,
68
+ "--valid",
69
+ "-v",
70
+ help="Send to validation dataset."
71
+ ),
72
+ test: bool = typer.Option(
73
+ False,
74
+ "--test",
75
+ "-t",
76
+ help="Send to test dataset."
77
+ ),
78
+ all: bool = typer.Option(
79
+ False,
80
+ "--all",
81
+ "-a",
82
+ help=(
83
+ "Send to both validation and test datasets. "
84
+ "It will send to validation and immediately resend to test. "
85
+ "If --test flag is passed, it will send to test set and resend to validation. "
86
+ ),
87
+ ),
88
+ safe: bool = typer.Option(
89
+ False,
90
+ "--safe",
91
+ "-s",
92
+ help="If there are many completions starting with the same prefix, throw an error instead of taking alphabetically first one."
93
+ ),
94
+ open_browser: bool = typer.Option(
95
+ False,
96
+ "--open",
97
+ "-o",
98
+ help="Open the submission in browser. Might not work on some platforms."
99
+ ),
100
+ task_path: Path = typer.Option(
101
+ Path.cwd(),
102
+ "--location",
103
+ "-l",
104
+ help="Path where the task is located (defaults to current directory)."
105
+ ),
106
+ backend: Optional[str] = typer.Option(
107
+ None,
108
+ "--backend",
109
+ "-b",
110
+ help=(
111
+ "Use a different backend: "
112
+ "local (l, l1, l2, ...), "
113
+ "remote (r, r1, r2, ...) "
114
+ "or full URL (https://example.com)"
115
+ )
116
+ )
117
+ ):
118
+ """
119
+ Send a submit to the current task.
120
+
121
+ If no flag specifing which dataset to send to is provided (--test, --valid, --all),
122
+ then it will default to validation set if its submission limit is not 0, and otherwise test set.
123
+ """
124
+
125
+ backend = resolve_backend(backend)
126
+ client = APIClient(backend)
127
+ fontend_url = get_frontend_url(backend)
128
+
129
+ metadata = read_metadata(task_path)
130
+ dotsai = read_dotsai(task_path)
131
+ if 'id' not in dotsai.keys():
132
+ write_error('.sai file is broken (does not contain task id)')
133
+ return
134
+
135
+ task_id = dotsai['id']
136
+ task_slug_or_id = metadata.get('slug', task_id)
137
+
138
+ if 'submission_files' not in metadata:
139
+ write_error('Could not find "submission_files" in metadata.json. Please ensure the task is synced.')
140
+ if 'valid' not in metadata:
141
+ write_error('Could not find "valid" in metadata.json. Please ensure the task is synced.')
142
+ if 'test' not in metadata:
143
+ write_error('Could not find "test" in metadata.json. Please ensure the task is synced.')
144
+ if 'daily_submissions' not in metadata['valid']:
145
+ write_error('Could not find "valid.daily_submissions" in metadata.json. Please ensure the task is synced.')
146
+ if 'daily_submissions' not in metadata['test']:
147
+ write_error('Could not find "test.daily_submissions" in metadata.json. Please ensure the task is synced.')
148
+
149
+ is_valid = True
150
+ if valid and test:
151
+ write_error('You cannot pass both --valid and --test flag at once. Please use --all to automatically resend the submission.')
152
+ if all:
153
+ if test:
154
+ is_valid = False
155
+ else:
156
+ if test:
157
+ is_valid = False
158
+ elif not valid:
159
+ if metadata['valid']['daily_submissions'] == 0:
160
+ is_valid = False
161
+
162
+ sub_names = [f['name'] for f in metadata['submission_files']]
163
+ if len(sub_names) != len(paths):
164
+ write_error(f'The should be {len(sub_names)} file{"s" if len(sub_names) > 1 else ""} submitted for this task, but there {"were" if len(paths) > 1 else "was"} {len(paths)} file path{"s" if len(paths) > 1 else ""} provided.')
165
+
166
+ typer.echo(f'Sending to ', nl=False)
167
+ typer.secho(f'{fontend_url}/zadanie/{task_slug_or_id}', nl=False, fg='green')
168
+ typer.echo(' as ', nl=False)
169
+ typer.secho('valid' if is_valid else 'test', fg='blue', nl=False)
170
+ typer.echo(':')
171
+ files = []
172
+ for name, path in zip(sub_names, paths):
173
+ path = supath(task_path, path, safe)
174
+ typer.echo(' - ', nl=False)
175
+ typer.secho(f'{name}', nl=False, fg='green')
176
+ typer.echo(f': {path.parent}/', nl=False)
177
+ typer.secho(f'{path.name}', fg='blue')
178
+ files.append(("files", (name, open(path, "rb"))))
179
+
180
+ resp = client.post('/submissions', files=files, data={'task_id': task_id, 'type_': 'nonfinal' if is_valid else 'final'})
181
+
182
+ if resp.status_code not in [200, 201]:
183
+ handle_response_error(resp)
184
+
185
+ subid = resp.json()['submission_id']
186
+ link = f'{fontend_url}/zadanie/{task_slug_or_id}/zgloszenia/{subid}'
187
+ typer.echo('\nSubmission sent to ', nl=False)
188
+ typer.secho(link, fg='green')
189
+ if open_browser:
190
+ open_in_browser(link)
191
+
192
+ if all:
193
+ resp_test = client.post(f'/submissions/{subid}/resend')
194
+
195
+ if resp_test.status_code not in [200, 201]:
196
+ handle_response_error(resp_test)
197
+
198
+ subid_test = resp_test.json()['submission_id']
199
+ link_test = f'{fontend_url}/zadanie/{task_slug_or_id}/zgloszenia/{subid_test}'
200
+ typer.echo('Resent to ', nl=False)
201
+ typer.secho(link_test, fg='green')
202
+
203
+ if open_browser:
204
+ webbrowser.open_new_tab(link_test)