divbase-cli 0.1.0a1__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.
- divbase_cli/__init__.py +1 -0
- divbase_cli/cli_commands/__init__.py +4 -0
- divbase_cli/cli_commands/auth_cli.py +95 -0
- divbase_cli/cli_commands/dimensions_cli.py +472 -0
- divbase_cli/cli_commands/file_cli.py +632 -0
- divbase_cli/cli_commands/query_cli.py +300 -0
- divbase_cli/cli_commands/shared_args_options.py +23 -0
- divbase_cli/cli_commands/task_history_cli.py +125 -0
- divbase_cli/cli_commands/user_config_cli.py +145 -0
- divbase_cli/cli_commands/version_cli.py +214 -0
- divbase_cli/cli_config.py +71 -0
- divbase_cli/cli_exceptions.py +166 -0
- divbase_cli/config_resolver.py +86 -0
- divbase_cli/display_task_history.py +197 -0
- divbase_cli/divbase_cli.py +76 -0
- divbase_cli/retries.py +34 -0
- divbase_cli/services/__init__.py +0 -0
- divbase_cli/services/announcements.py +46 -0
- divbase_cli/services/pre_signed_urls.py +460 -0
- divbase_cli/services/project_versions.py +103 -0
- divbase_cli/services/s3_files.py +444 -0
- divbase_cli/user_auth.py +380 -0
- divbase_cli/user_config.py +177 -0
- divbase_cli/utils.py +28 -0
- divbase_cli-0.1.0a1.dist-info/METADATA +44 -0
- divbase_cli-0.1.0a1.dist-info/RECORD +28 -0
- divbase_cli-0.1.0a1.dist-info/WHEEL +4 -0
- divbase_cli-0.1.0a1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query subcommand for the DivBase CLI.
|
|
3
|
+
|
|
4
|
+
Submits queries (sample metadata and/or bcftools) to the DivBase API.
|
|
5
|
+
|
|
6
|
+
If sample metadata query:
|
|
7
|
+
results are printed to the console.
|
|
8
|
+
|
|
9
|
+
If bcftools query:
|
|
10
|
+
a task id is returned which can be used to check the status of the job.
|
|
11
|
+
After task completed, a merged VCF file will be added to the project's storage bucket which can be downloaded by the user.
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TODO:
|
|
15
|
+
- Ability to download results file given task id with the file cli?
|
|
16
|
+
-
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
from rich import print
|
|
24
|
+
|
|
25
|
+
from divbase_cli.cli_commands.shared_args_options import PROJECT_NAME_OPTION
|
|
26
|
+
from divbase_cli.cli_config import cli_settings
|
|
27
|
+
from divbase_cli.config_resolver import ensure_logged_in, resolve_project
|
|
28
|
+
from divbase_cli.user_auth import make_authenticated_request
|
|
29
|
+
from divbase_lib.api_schemas.queries import (
|
|
30
|
+
BcftoolsQueryRequest,
|
|
31
|
+
SampleMetadataQueryRequest,
|
|
32
|
+
SampleMetadataQueryTaskResult,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
METADATA_TSV_ARGUMENT = typer.Option(
|
|
39
|
+
cli_settings.METADATA_TSV_NAME, help="Name of the sample metadata TSV file in the project's data store on DivBase."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
BCFTOOLS_ARGUMENT = typer.Option(
|
|
43
|
+
...,
|
|
44
|
+
help="""
|
|
45
|
+
String consisting of the bcftools view command(s) to run. E.g. "view -r 21:15000000-25000000" or "view -s".
|
|
46
|
+
The string cannot be empty; if you only want to subset on the selected samples, use: --command "view -s"
|
|
47
|
+
""",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Sample metadata and VCF queries both use the same core text, so it is defined up here.
|
|
51
|
+
TSV_FILTER_SYNTAX = (
|
|
52
|
+
"String consisting of keys:values in the tsv file to filter on. "
|
|
53
|
+
"The syntax is 'Key1:Value1,Value2;Key2:Value3,Value4', where the keys are the column header names in the tsv, "
|
|
54
|
+
"and values are the column values. Multiple values for a key are separated by commas, and multiple keys are "
|
|
55
|
+
"separated by semicolons. When multiple keys are provided, an intersect query will be performed. "
|
|
56
|
+
"E.g. 'Area:West of Ireland,Northern Portugal;Sex:F'."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
TSV_FILTER_HELP_TEXT_VCF = (
|
|
60
|
+
"This option calculates the samples to filter the VCFs on based on a sample metadata query. "
|
|
61
|
+
+ TSV_FILTER_SYNTAX
|
|
62
|
+
+ "\n\nMutually exclusive with --samples, --samples-file, and --all-samples."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
SAMPLE_SELECTION_HELP_PANEL = "Sample Selection (Required: Include Exactly One)"
|
|
66
|
+
VCF_QUERY_HELP_TEXT = (
|
|
67
|
+
"Submit a VCF query to run on the DivBase API. "
|
|
68
|
+
"A single, merged VCF file with the query results will be added to the project on success.\n\n"
|
|
69
|
+
"Exactly one sample-selection mode is required: "
|
|
70
|
+
"--tsv-filter | --samples | --samples-file | --all-samples."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
query_app = typer.Typer(
|
|
74
|
+
help="Run queries on the VCF files stored in the project's data store on DivBase. Queries are run on the DivBase API",
|
|
75
|
+
no_args_is_help=True,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@query_app.command("tsv")
|
|
80
|
+
def sample_metadata_query(
|
|
81
|
+
tsv_filter: str = typer.Argument(
|
|
82
|
+
...,
|
|
83
|
+
help=TSV_FILTER_SYNTAX,
|
|
84
|
+
),
|
|
85
|
+
show_sample_results: bool = typer.Option(
|
|
86
|
+
default=False,
|
|
87
|
+
help="Print sample_ID and Filename results from the query.",
|
|
88
|
+
),
|
|
89
|
+
metadata_tsv_name: str = METADATA_TSV_ARGUMENT,
|
|
90
|
+
project: str | None = PROJECT_NAME_OPTION,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Query the tsv sidecar metadata file for the VCF files in the project's data store on DivBase.
|
|
94
|
+
Returns the sample IDs and filenames that match the query.
|
|
95
|
+
"""
|
|
96
|
+
project_config = resolve_project(project_name=project)
|
|
97
|
+
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
98
|
+
|
|
99
|
+
request_data = SampleMetadataQueryRequest(tsv_filter=tsv_filter, metadata_tsv_name=metadata_tsv_name)
|
|
100
|
+
|
|
101
|
+
response = make_authenticated_request(
|
|
102
|
+
method="POST",
|
|
103
|
+
divbase_base_url=logged_in_url,
|
|
104
|
+
api_route=f"v1/query/sample-metadata/projects/{project_config.name}",
|
|
105
|
+
json=request_data.model_dump(),
|
|
106
|
+
timeout=20, # This is longer than default (5), as api call response is query result, not a task-id.
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
results = SampleMetadataQueryTaskResult(**response.json())
|
|
110
|
+
|
|
111
|
+
if results.warnings:
|
|
112
|
+
print("[yellow]Warnings:[/yellow]")
|
|
113
|
+
for warning in results.warnings:
|
|
114
|
+
print(f" • {warning}")
|
|
115
|
+
print()
|
|
116
|
+
|
|
117
|
+
if show_sample_results:
|
|
118
|
+
print("[bright_blue]Name and file for each sample in query results:[/bright_blue]")
|
|
119
|
+
for sample in results.sample_and_filename_subset:
|
|
120
|
+
print(f"Sample ID: '{sample.sample_id}', Filename: '{sample.filename}'")
|
|
121
|
+
|
|
122
|
+
print(f"The results for the query ([bright_blue]{results.query_message}[/bright_blue]):")
|
|
123
|
+
|
|
124
|
+
unique_sample_ids = results.unique_sample_ids or []
|
|
125
|
+
unique_filenames = results.unique_filenames or []
|
|
126
|
+
print(f"Unique Sample IDs: {unique_sample_ids}")
|
|
127
|
+
print(f"Unique filenames: {unique_filenames}\n")
|
|
128
|
+
if not unique_sample_ids:
|
|
129
|
+
print("[yellow]No samples match your query filters.[/yellow]\n")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@query_app.command("vcf", help=VCF_QUERY_HELP_TEXT)
|
|
133
|
+
def vcf_query(
|
|
134
|
+
tsv_filter: str | None = typer.Option(
|
|
135
|
+
None, help=TSV_FILTER_HELP_TEXT_VCF, rich_help_panel=SAMPLE_SELECTION_HELP_PANEL
|
|
136
|
+
),
|
|
137
|
+
samples: str | None = typer.Option(
|
|
138
|
+
None,
|
|
139
|
+
help="Comma-separated list of sample IDs. Mutually exclusive with --tsv-filter, --samples-file, and --all-samples.",
|
|
140
|
+
rich_help_panel=SAMPLE_SELECTION_HELP_PANEL,
|
|
141
|
+
),
|
|
142
|
+
samples_file: Path | None = typer.Option(
|
|
143
|
+
None,
|
|
144
|
+
exists=True,
|
|
145
|
+
file_okay=True,
|
|
146
|
+
dir_okay=False,
|
|
147
|
+
readable=True,
|
|
148
|
+
resolve_path=True,
|
|
149
|
+
help=(
|
|
150
|
+
"Path to a UTF-8 text file with one sample ID per line. Blank lines and lines starting with # are ignored. "
|
|
151
|
+
"Mutually exclusive with --tsv-filter, --samples, and --all-samples."
|
|
152
|
+
),
|
|
153
|
+
rich_help_panel=SAMPLE_SELECTION_HELP_PANEL,
|
|
154
|
+
),
|
|
155
|
+
all_samples: bool = typer.Option(
|
|
156
|
+
False,
|
|
157
|
+
"--all-samples",
|
|
158
|
+
help=(
|
|
159
|
+
"Use all samples in the project for the query. "
|
|
160
|
+
"Mutually exclusive with --tsv-filter, --samples, and --samples-file."
|
|
161
|
+
),
|
|
162
|
+
rich_help_panel=SAMPLE_SELECTION_HELP_PANEL,
|
|
163
|
+
),
|
|
164
|
+
command: str = BCFTOOLS_ARGUMENT,
|
|
165
|
+
metadata_tsv_name: str = METADATA_TSV_ARGUMENT,
|
|
166
|
+
project: str | None = PROJECT_NAME_OPTION,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""
|
|
169
|
+
Submit a VCF query to run on the DivBase API. A single, merged VCF file with the query results will be added to the project on success.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
# TODO Error handling for subprocess calls.
|
|
173
|
+
# TODO: handle the case empty results are returned from tsv_query()
|
|
174
|
+
# TODO what if a job fails and the user wants to re-run it? do we store temp files?
|
|
175
|
+
|
|
176
|
+
# Note! Pydantic model validator also enforces this on the API side just queries can be submitted directly to the endpoint.
|
|
177
|
+
# This block here is to catch it on the CLI side with a more user-friendly error message before even making the API call.
|
|
178
|
+
has_tsv_filter = tsv_filter is not None
|
|
179
|
+
has_samples = samples is not None
|
|
180
|
+
has_samples_file = samples_file is not None
|
|
181
|
+
has_all_samples = all_samples
|
|
182
|
+
selection_count = sum([has_tsv_filter, has_samples, has_samples_file, has_all_samples])
|
|
183
|
+
if selection_count > 1:
|
|
184
|
+
raise typer.BadParameter("Use only one of --tsv-filter, --samples, --samples-file, or --all-samples.")
|
|
185
|
+
if selection_count == 0:
|
|
186
|
+
raise typer.BadParameter(
|
|
187
|
+
"Sample selection is required. Use one of --tsv-filter, --samples, --samples-file, or --all-samples."
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
normalized_samples, sample_input_warnings = _normalize_samples_input(samples=samples, samples_file=samples_file)
|
|
191
|
+
if sample_input_warnings:
|
|
192
|
+
print("[yellow]Warnings:[/yellow]")
|
|
193
|
+
for warning in sample_input_warnings:
|
|
194
|
+
print(f" • {warning}")
|
|
195
|
+
print()
|
|
196
|
+
|
|
197
|
+
project_config = resolve_project(project_name=project)
|
|
198
|
+
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
199
|
+
|
|
200
|
+
request_data = BcftoolsQueryRequest(
|
|
201
|
+
tsv_filter=tsv_filter,
|
|
202
|
+
command=command,
|
|
203
|
+
metadata_tsv_name=metadata_tsv_name,
|
|
204
|
+
samples=normalized_samples,
|
|
205
|
+
all_samples=all_samples,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
response = make_authenticated_request(
|
|
209
|
+
method="POST",
|
|
210
|
+
divbase_base_url=logged_in_url,
|
|
211
|
+
api_route=f"v1/query/vcf/projects/{project_config.name}",
|
|
212
|
+
json=request_data.model_dump(),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
task_id = response.json()
|
|
216
|
+
print(
|
|
217
|
+
f"Job submitted successfully with task id: {task_id}. To check the status of your job, use the command: divbase-cli task-history id {task_id}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _normalize_samples_input(samples: str | None, samples_file: Path | None) -> tuple[list[str] | None, list[str]]:
|
|
222
|
+
"""
|
|
223
|
+
Normalize sample selection inputs from CLI options.
|
|
224
|
+
"""
|
|
225
|
+
if samples is not None:
|
|
226
|
+
normalized = []
|
|
227
|
+
for sample in samples.split(","):
|
|
228
|
+
sample_clean = sample.strip()
|
|
229
|
+
if sample_clean:
|
|
230
|
+
normalized.append(sample_clean)
|
|
231
|
+
|
|
232
|
+
if not normalized:
|
|
233
|
+
raise typer.BadParameter("--samples must contain at least one non-empty sample ID.")
|
|
234
|
+
return normalized, []
|
|
235
|
+
|
|
236
|
+
if samples_file is not None:
|
|
237
|
+
raw_lines = samples_file.read_text(encoding="utf-8").splitlines()
|
|
238
|
+
normalized = []
|
|
239
|
+
warnings = []
|
|
240
|
+
delimiter_lines = []
|
|
241
|
+
found_delimiters = set()
|
|
242
|
+
disallowed_delimiters = (",", ";", "\t", "|")
|
|
243
|
+
delimiter_display = {
|
|
244
|
+
",": "','",
|
|
245
|
+
";": "';'",
|
|
246
|
+
"\t": "'tab'",
|
|
247
|
+
"|": "'|'",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for line_number, raw_line in enumerate(raw_lines, start=1):
|
|
251
|
+
stripped = raw_line.strip()
|
|
252
|
+
if not stripped or stripped.startswith("#"):
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
normalized.append(stripped)
|
|
256
|
+
line_delimiters = set()
|
|
257
|
+
for delimiter in disallowed_delimiters:
|
|
258
|
+
if delimiter in stripped:
|
|
259
|
+
line_delimiters.add(delimiter)
|
|
260
|
+
|
|
261
|
+
if line_delimiters:
|
|
262
|
+
delimiter_lines.append((line_number, stripped))
|
|
263
|
+
found_delimiters.update(line_delimiters)
|
|
264
|
+
|
|
265
|
+
if not normalized:
|
|
266
|
+
raise typer.BadParameter(
|
|
267
|
+
f"Samples file has no sample IDs after ignoring blank/comment lines: {samples_file}. Please ensure that it contains at least one sample ID."
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
if delimiter_lines:
|
|
271
|
+
preview_entries = [] # Only preview up to the first 3 lines with delimiters to avoid overwhelming the user with a long list if there are many problematic lines.
|
|
272
|
+
for line_number, line_value in delimiter_lines[:3]:
|
|
273
|
+
preview_entries.append(f"line {line_number} ('{line_value}')")
|
|
274
|
+
preview = ", ".join(preview_entries)
|
|
275
|
+
|
|
276
|
+
previewed_line_count = min(len(delimiter_lines), 3)
|
|
277
|
+
extra_line_count = len(delimiter_lines) - previewed_line_count
|
|
278
|
+
extra_msg = f", +{extra_line_count} more line(s)" if extra_line_count > 0 else ""
|
|
279
|
+
|
|
280
|
+
delimiter_names = []
|
|
281
|
+
for delimiter in disallowed_delimiters:
|
|
282
|
+
if delimiter in found_delimiters:
|
|
283
|
+
delimiter_names.append(delimiter_display[delimiter])
|
|
284
|
+
delimiters_found_str = ", ".join(delimiter_names)
|
|
285
|
+
|
|
286
|
+
raise typer.BadParameter(
|
|
287
|
+
"Invalid --samples-file format: expected one sample ID per line with no delimiters. "
|
|
288
|
+
f"Found delimiter(s) {delimiters_found_str} on {len(delimiter_lines)} line(s): \n{preview}{extra_msg}. "
|
|
289
|
+
"\nPlease do not use delimiters (',' ';' '\\t' '|') in your samples file."
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
if len(normalized) == 1:
|
|
293
|
+
warnings.append(
|
|
294
|
+
"Only one sample ID was found in --samples-file after ignoring blank/comment lines. "
|
|
295
|
+
"If this was not intended, verify that the file has one sample ID per line."
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return normalized, warnings
|
|
299
|
+
|
|
300
|
+
return None, []
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
To avoid potential problems with circular imports, we can put shared typer args + options (etc...) here
|
|
3
|
+
|
|
4
|
+
If you have something that is only used in one of the cli subcommands, don't move it here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
PROJECT_NAME_OPTION = typer.Option(
|
|
10
|
+
None,
|
|
11
|
+
"--project",
|
|
12
|
+
"-p",
|
|
13
|
+
help="Name of the DivBase project, if not provided uses the default in your DivBase config file",
|
|
14
|
+
show_default=False,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
FORMAT_AS_TSV_OPTION = typer.Option(
|
|
19
|
+
False,
|
|
20
|
+
"--tsv",
|
|
21
|
+
"-t",
|
|
22
|
+
help="If set, will print the output in .TSV format for easier programmatic parsing.",
|
|
23
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task history subcommand for the DivBase CLI.
|
|
3
|
+
|
|
4
|
+
Submits a query for fetching the Celery task history for the user to the DivBase API.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from divbase_cli.cli_commands.shared_args_options import FORMAT_AS_TSV_OPTION
|
|
13
|
+
from divbase_cli.config_resolver import ensure_logged_in, resolve_project, resolve_url_for_non_project_specific_commands
|
|
14
|
+
from divbase_cli.display_task_history import TaskHistoryDisplayManager
|
|
15
|
+
from divbase_cli.user_auth import make_authenticated_request
|
|
16
|
+
from divbase_cli.user_config import load_user_config
|
|
17
|
+
from divbase_lib.api_schemas.task_history import TaskHistoryResult
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
task_history_app = typer.Typer(
|
|
23
|
+
help="Get the task history of query jobs submitted by the user to the DivBase API.",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@task_history_app.command("user")
|
|
29
|
+
def list_task_history_for_user(
|
|
30
|
+
format_output_as_tsv: bool = FORMAT_AS_TSV_OPTION,
|
|
31
|
+
limit: int = typer.Option(10, help="Maximum number of tasks to display in the terminal. Sorted by recency."),
|
|
32
|
+
project: str | None = typer.Option(
|
|
33
|
+
None, help="Optional project name to filter the user's task history by project."
|
|
34
|
+
),
|
|
35
|
+
):
|
|
36
|
+
"""
|
|
37
|
+
Check status of all tasks submitted by the user. Displays the latest 10 tasks by default, unless --limit is specified. Can be filtered by project name.
|
|
38
|
+
"""
|
|
39
|
+
# TODO add option to sort ASC/DESC by task timestamp
|
|
40
|
+
divbase_url = resolve_url_for_non_project_specific_commands()
|
|
41
|
+
|
|
42
|
+
if project:
|
|
43
|
+
task_history_response = make_authenticated_request(
|
|
44
|
+
method="GET",
|
|
45
|
+
divbase_base_url=divbase_url,
|
|
46
|
+
api_route=f"v1/task-history/tasks/user/projects/{project}",
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
task_history_response = make_authenticated_request(
|
|
50
|
+
method="GET",
|
|
51
|
+
divbase_base_url=divbase_url,
|
|
52
|
+
api_route="v1/task-history/tasks/user",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
task_history_data = [TaskHistoryResult(**item) for item in task_history_response.json()]
|
|
56
|
+
|
|
57
|
+
config = load_user_config()
|
|
58
|
+
TaskHistoryDisplayManager(
|
|
59
|
+
task_items=task_history_data,
|
|
60
|
+
user_email=config.logged_in_email,
|
|
61
|
+
project_name=project,
|
|
62
|
+
mode="user_project" if project else "user",
|
|
63
|
+
display_limit=limit,
|
|
64
|
+
format_output_as_tsv=format_output_as_tsv,
|
|
65
|
+
).print_task_history()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@task_history_app.command("id")
|
|
69
|
+
def task_history_by_id(
|
|
70
|
+
task_id: int | None = typer.Argument(..., help="Task ID to check the status of a specific query job."),
|
|
71
|
+
format_output_as_tsv: bool = FORMAT_AS_TSV_OPTION,
|
|
72
|
+
):
|
|
73
|
+
"""
|
|
74
|
+
Check status of a specific task submitted by the user by its task ID.
|
|
75
|
+
"""
|
|
76
|
+
divbase_url = resolve_url_for_non_project_specific_commands()
|
|
77
|
+
|
|
78
|
+
task_history_response = make_authenticated_request(
|
|
79
|
+
method="GET",
|
|
80
|
+
divbase_base_url=divbase_url,
|
|
81
|
+
api_route=f"v1/task-history/tasks/{task_id}",
|
|
82
|
+
)
|
|
83
|
+
task_history_data = [TaskHistoryResult(**item) for item in task_history_response.json()]
|
|
84
|
+
|
|
85
|
+
TaskHistoryDisplayManager(
|
|
86
|
+
task_items=task_history_data,
|
|
87
|
+
user_email=None,
|
|
88
|
+
project_name=None,
|
|
89
|
+
mode="id",
|
|
90
|
+
format_output_as_tsv=format_output_as_tsv,
|
|
91
|
+
).print_task_history()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@task_history_app.command("project")
|
|
95
|
+
def list_task_history_for_project(
|
|
96
|
+
project: str = typer.Argument(
|
|
97
|
+
None,
|
|
98
|
+
help="Project name to check the task history for. Leave blank to use the default project set in your config.",
|
|
99
|
+
),
|
|
100
|
+
limit: int = typer.Option(10, help="Maximum number of tasks to display in the terminal. Sorted by recency."),
|
|
101
|
+
format_output_as_tsv: bool = FORMAT_AS_TSV_OPTION,
|
|
102
|
+
):
|
|
103
|
+
"""
|
|
104
|
+
Check status of all tasks submitted for a project. Requires a manager role in the project. Displays the latest 10 tasks by default, unless --limit is specified.
|
|
105
|
+
"""
|
|
106
|
+
# TODO add option to sort ASC/DESC by task timestamp
|
|
107
|
+
project_config = resolve_project(project_name=project)
|
|
108
|
+
logged_in_url = ensure_logged_in(desired_url=project_config.divbase_url)
|
|
109
|
+
|
|
110
|
+
task_history_response = make_authenticated_request(
|
|
111
|
+
method="GET",
|
|
112
|
+
divbase_base_url=logged_in_url,
|
|
113
|
+
api_route=f"v1/task-history/projects/{project_config.name}",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
task_history_data = [TaskHistoryResult(**item) for item in task_history_response.json()]
|
|
117
|
+
|
|
118
|
+
TaskHistoryDisplayManager(
|
|
119
|
+
task_items=task_history_data,
|
|
120
|
+
user_email=None,
|
|
121
|
+
project_name=project_config.name,
|
|
122
|
+
mode="project",
|
|
123
|
+
display_limit=limit,
|
|
124
|
+
format_output_as_tsv=format_output_as_tsv,
|
|
125
|
+
).print_task_history()
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
config subcommand for the divbase-cli package.
|
|
3
|
+
|
|
4
|
+
Controls the user config file stored at "~/.config/.divbase_tools.yaml" (unless specified otherwise).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich import print
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from divbase_cli.cli_config import cli_settings
|
|
13
|
+
from divbase_cli.user_config import (
|
|
14
|
+
load_user_config,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
config_app = typer.Typer(help="Manage your user configuration file for the DivBase CLI.", no_args_is_help=True)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@config_app.command("add")
|
|
21
|
+
def add_project_command(
|
|
22
|
+
name: str = typer.Argument(..., help="Name of the project to add to your config file."),
|
|
23
|
+
divbase_url: str = typer.Option(
|
|
24
|
+
cli_settings.DIVBASE_API_URL,
|
|
25
|
+
"--divbase-url",
|
|
26
|
+
"-u",
|
|
27
|
+
help="DivBase API URL associated with this project.",
|
|
28
|
+
),
|
|
29
|
+
make_default: bool = typer.Option(
|
|
30
|
+
False,
|
|
31
|
+
"--default",
|
|
32
|
+
"-d",
|
|
33
|
+
help="Set this project as the default project in your config file.",
|
|
34
|
+
),
|
|
35
|
+
):
|
|
36
|
+
"""Add a new project to your user configuration file."""
|
|
37
|
+
config = load_user_config()
|
|
38
|
+
project = config.add_project(
|
|
39
|
+
name=name,
|
|
40
|
+
divbase_url=divbase_url,
|
|
41
|
+
is_default=make_default,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
print(f"Project: '{project.name}' added to your config.")
|
|
45
|
+
print(f"The URL: {project.divbase_url} was set as the DivBase API URL for this project.")
|
|
46
|
+
|
|
47
|
+
if make_default:
|
|
48
|
+
print(f"Project '{project.name}' is now set as your default project")
|
|
49
|
+
else:
|
|
50
|
+
print(f"To make '{project.name}' your default project you can run: 'divbase config set-default {project.name}'")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@config_app.command("rm")
|
|
54
|
+
def remove_project_command(
|
|
55
|
+
name: str = typer.Argument(..., help="Name of the project to remove from your user configuration file."),
|
|
56
|
+
):
|
|
57
|
+
"""Remove a project from your user configuration file."""
|
|
58
|
+
config = load_user_config()
|
|
59
|
+
removed_project = config.remove_project(name)
|
|
60
|
+
|
|
61
|
+
if not removed_project:
|
|
62
|
+
print(f"Nothing to do, the project '{name}' was not found in your user config")
|
|
63
|
+
else:
|
|
64
|
+
print(f"The project '{removed_project}' was removed from your config.")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@config_app.command("set-default")
|
|
68
|
+
def set_default_project_command(
|
|
69
|
+
name: str = typer.Argument(..., help="Name of the project to add to the user configuration file."),
|
|
70
|
+
):
|
|
71
|
+
"""Set your default project to use in all divbase-cli commands."""
|
|
72
|
+
config = load_user_config()
|
|
73
|
+
default_project_name = config.set_default_project(name=name)
|
|
74
|
+
print(f"Default project is now set to '{default_project_name}'.")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@config_app.command("show-default")
|
|
78
|
+
def show_default_project_command() -> None:
|
|
79
|
+
"""Print the currently set default project to the console."""
|
|
80
|
+
config = load_user_config()
|
|
81
|
+
|
|
82
|
+
if config.default_project:
|
|
83
|
+
print(config.default_project)
|
|
84
|
+
else:
|
|
85
|
+
print("No default project is set in the user configuration file.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@config_app.command("set-dload-dir")
|
|
89
|
+
def set_default_dload_dir_command(
|
|
90
|
+
download_dir: str = typer.Argument(
|
|
91
|
+
...,
|
|
92
|
+
help="""Set the default directory to download files to.
|
|
93
|
+
By default files are downloaded to the current working directory.
|
|
94
|
+
You can specify an absolute path.
|
|
95
|
+
You can use '.' to refer to the directory you run the command from.""",
|
|
96
|
+
),
|
|
97
|
+
):
|
|
98
|
+
"""Set the default download dir"""
|
|
99
|
+
config = load_user_config()
|
|
100
|
+
dload_dir = config.set_default_download_dir(download_dir=download_dir)
|
|
101
|
+
if dload_dir == ".":
|
|
102
|
+
print("The default download directory will be whereever you run the command from.")
|
|
103
|
+
else:
|
|
104
|
+
print(f"The default download directory is now set to: {dload_dir}.")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@config_app.command("show")
|
|
108
|
+
def show_user_config():
|
|
109
|
+
"""Pretty print the contents of your current config file."""
|
|
110
|
+
config = load_user_config()
|
|
111
|
+
console = Console()
|
|
112
|
+
|
|
113
|
+
console.print(
|
|
114
|
+
f"[bold]Your DivBase user configuration file's contents located at:[/bold] '{cli_settings.CONFIG_PATH.resolve()}'\n"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if not config.default_download_dir:
|
|
118
|
+
dload_dir_info = "Not specified, meaning the working directory of wherever you run the download command from."
|
|
119
|
+
elif config.default_download_dir == ".":
|
|
120
|
+
dload_dir_info = "Working directory of wherever you run the download command from."
|
|
121
|
+
else:
|
|
122
|
+
dload_dir_info = config.default_download_dir
|
|
123
|
+
console.print(f"[bold]Default Download Directory:[/bold] '{dload_dir_info}'")
|
|
124
|
+
|
|
125
|
+
if config.logged_in_url and config.logged_in_email:
|
|
126
|
+
console.print(f"[bold]You're logged into a DivBase server at URL:[/bold] '{config.logged_in_url}'")
|
|
127
|
+
console.print(f"[bold]Logged in with email:[/bold] '{config.logged_in_email}'")
|
|
128
|
+
else:
|
|
129
|
+
console.print("[bold]You're not logged into any DivBase server.[/bold]")
|
|
130
|
+
|
|
131
|
+
if not config.projects:
|
|
132
|
+
console.print("[bold]No projects defined in your user config file.[/bold]")
|
|
133
|
+
console.print("You can add a project using the command: 'divbase-cli config add <project_name>'")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
table = Table(title="\nProjects in your DivBase CLI user config file")
|
|
137
|
+
table.add_column("Project Name", style="cyan")
|
|
138
|
+
table.add_column("DivBase URL", style="green")
|
|
139
|
+
table.add_column("Is default", style="yellow")
|
|
140
|
+
|
|
141
|
+
for project in config.projects:
|
|
142
|
+
is_default = "Yes" if project.name == config.default_project else ""
|
|
143
|
+
table.add_row(project.name, project.divbase_url, is_default)
|
|
144
|
+
|
|
145
|
+
console.print(table)
|