dds-cli 2.13.0__tar.gz → 2.14.2__tar.gz
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.
- {dds_cli-2.13.0 → dds_cli-2.14.2}/PKG-INFO +13 -15
- {dds_cli-2.13.0 → dds_cli-2.14.2}/README.md +4 -4
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/__init__.py +1 -1
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/__main__.py +81 -82
- dds_cli-2.14.2/dds_cli/constants.py +25 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/custom_decorators.py +1 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/data_getter.py +68 -25
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/data_lister.py +21 -24
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/data_putter.py +18 -12
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/file_compressor.py +5 -7
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/file_encryptor.py +1 -1
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/file_handler_local.py +22 -25
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/file_handler_remote.py +2 -4
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/project_info.py +1 -1
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/project_status.py +12 -3
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/user.py +4 -4
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/utils.py +4 -6
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/version.py +1 -1
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli.egg-info/PKG-INFO +13 -15
- dds_cli-2.14.2/dds_cli.egg-info/SOURCES.txt +62 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli.egg-info/requires.txt +9 -8
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli.egg-info/top_level.txt +0 -1
- dds_cli-2.14.2/pyproject.toml +16 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/setup.py +5 -8
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_auth.py +18 -16
- dds_cli-2.14.2/tests/test_commands.py +185 -0
- dds_cli-2.14.2/tests/test_data_getter.py +314 -0
- dds_cli-2.14.2/tests/test_data_putter.py +85 -0
- dds_cli-2.14.2/tests/test_decorators.py +129 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_file_handler_local.py +118 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_project_status.py +133 -0
- dds_cli-2.13.0/dds_cli/constants.py +0 -14
- dds_cli-2.13.0/dds_cli/dds_gui/__init__.py +0 -6
- dds_cli-2.13.0/dds_cli/dds_gui/app.py +0 -71
- dds_cli-2.13.0/dds_cli/dds_gui/components/__init__.py +0 -1
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_button.py +0 -65
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_container.py +0 -80
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_footer.py +0 -17
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_form.py +0 -27
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_input.py +0 -19
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_modal.py +0 -138
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_select.py +0 -23
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_status_chip.py +0 -61
- dds_cli-2.13.0/dds_cli/dds_gui/components/dds_text_item.py +0 -17
- dds_cli-2.13.0/dds_cli/dds_gui/dds_state_manager.py +0 -165
- dds_cli-2.13.0/dds_cli/dds_gui/pages/__init__.py +0 -1
- dds_cli-2.13.0/dds_cli/dds_gui/pages/authentication/__init__.py +0 -1
- dds_cli-2.13.0/dds_cli/dds_gui/pages/authentication/authentication.py +0 -56
- dds_cli-2.13.0/dds_cli/dds_gui/pages/authentication/authentication_form.py +0 -138
- dds_cli-2.13.0/dds_cli/dds_gui/pages/authentication/modals/__init__.py +0 -1
- dds_cli-2.13.0/dds_cli/dds_gui/pages/authentication/modals/login_modal.py +0 -35
- dds_cli-2.13.0/dds_cli/dds_gui/pages/authentication/modals/logout_modal.py +0 -28
- dds_cli-2.13.0/dds_cli/dds_gui/pages/authentication/modals/reauthenticate_modal.py +0 -35
- dds_cli-2.13.0/dds_cli/dds_gui/pages/project_view.py +0 -69
- dds_cli-2.13.0/dds_cli/dds_gui/pages/project_view_mode/__init__.py +0 -1
- dds_cli-2.13.0/dds_cli/dds_gui/pages/project_view_mode/project_actions.py +0 -32
- dds_cli-2.13.0/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/__init__.py +0 -1
- dds_cli-2.13.0/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/download_data.py +0 -61
- dds_cli-2.13.0/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/user_access.py +0 -41
- dds_cli-2.13.0/dds_cli/dds_gui/types/__init__.py +0 -1
- dds_cli-2.13.0/dds_cli/dds_gui/types/dds_severity_types.py +0 -12
- dds_cli-2.13.0/dds_cli/dds_gui/types/dds_status_types.py +0 -14
- dds_cli-2.13.0/dds_cli.egg-info/SOURCES.txt +0 -97
- dds_cli-2.13.0/gui_build/gui_standalone.py +0 -11
- dds_cli-2.13.0/pyproject.toml +0 -3
- dds_cli-2.13.0/tests/__init__.py +0 -0
- dds_cli-2.13.0/tests/gui_tests/__init__.py +0 -0
- dds_cli-2.13.0/tests/gui_tests/test_authentication.py +0 -1567
- dds_cli-2.13.0/tests/gui_tests/test_important_information.py +0 -458
- dds_cli-2.13.0/tests/gui_tests/test_project_content.py +0 -1000
- dds_cli-2.13.0/tests/gui_tests/test_project_information.py +0 -650
- dds_cli-2.13.0/tests/gui_tests/test_project_list.py +0 -558
- dds_cli-2.13.0/tests/test_data_getter.py +0 -133
- {dds_cli-2.13.0 → dds_cli-2.14.2}/LICENSE +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/account_manager.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/auth.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/base.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/data_remover.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/directory.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/exceptions.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/file_handler.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/message_helper.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/motd_manager.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/options.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/project_creator.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/s3_connector.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/status.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/superadmin_helper.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/text_handler.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/timestamp.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli/unit_manager.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli.egg-info/dependency_links.txt +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli.egg-info/entry_points.txt +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/dds_cli.egg-info/not-zip-safe +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/setup.cfg +0 -0
- {dds_cli-2.13.0/gui_build → dds_cli-2.14.2/tests}/__init__.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_account_manager.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_base.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_data_remover.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_file_compressor.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_file_encryptor.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_motd_manager.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_s3_connector.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_superadmin_helper.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_user.py +0 -0
- {dds_cli-2.13.0 → dds_cli-2.14.2}/tests/test_utils.py +0 -0
|
@@ -1,39 +1,37 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dds_cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.14.2
|
|
4
4
|
Summary: A command line tool to manage data and projects in the SciLifeLab Data Delivery System.
|
|
5
5
|
Home-page: https://github.com/ScilifelabDataCentre/dds_cli
|
|
6
6
|
Author: SciLifeLab Data Centre
|
|
7
7
|
License: MIT
|
|
8
8
|
Classifier: Development Status :: 4 - Beta
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
14
|
Description-Content-Type: text/markdown
|
|
16
15
|
License-File: LICENSE
|
|
17
16
|
Requires-Dist: boto3==1.24.73
|
|
18
17
|
Requires-Dist: botocore==1.27.73
|
|
19
18
|
Requires-Dist: click==8.1.3
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist: cryptography==44.0.1
|
|
19
|
+
Requires-Dist: cryptography==46.0.6
|
|
22
20
|
Requires-Dist: immutabledict==2.2.1
|
|
23
21
|
Requires-Dist: jwcrypto==1.5.6
|
|
24
22
|
Requires-Dist: prettytable==3.7.0
|
|
25
23
|
Requires-Dist: prompt-toolkit==3.0.40
|
|
26
|
-
Requires-Dist: PyNaCl==1.
|
|
27
|
-
Requires-Dist: pytz==
|
|
24
|
+
Requires-Dist: PyNaCl==1.6.2
|
|
25
|
+
Requires-Dist: pytz==2026.1.post1
|
|
28
26
|
Requires-Dist: PyYAML==6.0.2
|
|
29
|
-
Requires-Dist: questionary==1.
|
|
30
|
-
Requires-Dist: requests==2.
|
|
31
|
-
Requires-Dist: rich==
|
|
27
|
+
Requires-Dist: questionary==2.1.1
|
|
28
|
+
Requires-Dist: requests==2.33.0
|
|
29
|
+
Requires-Dist: rich==14.3.3
|
|
32
30
|
Requires-Dist: rich-click==1.5.2
|
|
33
31
|
Requires-Dist: simplejson==3.17.6
|
|
34
32
|
Requires-Dist: tzlocal==4.2
|
|
35
33
|
Requires-Dist: zstandard==0.23.0
|
|
36
|
-
Requires-Dist:
|
|
34
|
+
Requires-Dist: legacy-cgi==2.6.1; python_version >= "3.13"
|
|
37
35
|
Dynamic: author
|
|
38
36
|
Dynamic: classifier
|
|
39
37
|
Dynamic: description
|
|
@@ -61,15 +59,15 @@ Dynamic: summary
|
|
|
61
59
|
<a href="https://opensource.org/licenses/MIT">
|
|
62
60
|
<img alt="Licence: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg">
|
|
63
61
|
</a>
|
|
64
|
-
<a href="https://github.com/
|
|
65
|
-
<img alt="
|
|
62
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
63
|
+
<img alt="Linting: ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json">
|
|
66
64
|
</a>
|
|
67
65
|
<a href="https://prettier.io/">
|
|
68
66
|
<img alt="Code style: prettier" src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg">
|
|
69
67
|
</a>
|
|
70
68
|
<br />
|
|
71
|
-
<a href="https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/lint-
|
|
72
|
-
<img alt="
|
|
69
|
+
<a href="https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/lint-ruff.yml">
|
|
70
|
+
<img alt="Linting: ruff" src="https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/lint-ruff.yml/badge.svg?event=push">
|
|
73
71
|
</a>
|
|
74
72
|
<img alt="CodeQL" src="https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/codeql-cli.yml/badge.svg">
|
|
75
73
|
<a href="https://codecov.io/github/ScilifelabDataCentre/dds_cli" >
|
|
@@ -15,15 +15,15 @@
|
|
|
15
15
|
<a href="https://opensource.org/licenses/MIT">
|
|
16
16
|
<img alt="Licence: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg">
|
|
17
17
|
</a>
|
|
18
|
-
<a href="https://github.com/
|
|
19
|
-
<img alt="
|
|
18
|
+
<a href="https://github.com/astral-sh/ruff">
|
|
19
|
+
<img alt="Linting: ruff" src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json">
|
|
20
20
|
</a>
|
|
21
21
|
<a href="https://prettier.io/">
|
|
22
22
|
<img alt="Code style: prettier" src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg">
|
|
23
23
|
</a>
|
|
24
24
|
<br />
|
|
25
|
-
<a href="https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/lint-
|
|
26
|
-
<img alt="
|
|
25
|
+
<a href="https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/lint-ruff.yml">
|
|
26
|
+
<img alt="Linting: ruff" src="https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/lint-ruff.yml/badge.svg?event=push">
|
|
27
27
|
</a>
|
|
28
28
|
<img alt="CodeQL" src="https://github.com/ScilifelabDataCentre/dds_cli/actions/workflows/codeql-cli.yml/badge.svg">
|
|
29
29
|
<a href="https://codecov.io/github/ScilifelabDataCentre/dds_cli" >
|
|
@@ -8,61 +8,59 @@
|
|
|
8
8
|
import concurrent.futures
|
|
9
9
|
import itertools
|
|
10
10
|
import logging
|
|
11
|
+
import pathlib
|
|
11
12
|
import sys
|
|
12
13
|
|
|
13
14
|
# Installed
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
import click_pathlib
|
|
15
|
+
|
|
16
|
+
import questionary
|
|
17
17
|
import rich
|
|
18
18
|
import rich.logging
|
|
19
19
|
import rich.markup
|
|
20
20
|
import rich.progress
|
|
21
21
|
import rich.prompt
|
|
22
|
-
import
|
|
22
|
+
import rich_click as click
|
|
23
23
|
|
|
24
24
|
# Own modules
|
|
25
25
|
import dds_cli
|
|
26
26
|
import dds_cli.account_manager
|
|
27
|
-
import dds_cli.
|
|
28
|
-
import dds_cli.motd_manager
|
|
29
|
-
import dds_cli.superadmin_helper
|
|
27
|
+
import dds_cli.auth
|
|
30
28
|
import dds_cli.data_getter
|
|
31
29
|
import dds_cli.data_lister
|
|
32
30
|
import dds_cli.data_putter
|
|
33
31
|
import dds_cli.data_remover
|
|
34
32
|
import dds_cli.directory
|
|
33
|
+
import dds_cli.message_helper
|
|
34
|
+
import dds_cli.motd_manager
|
|
35
35
|
import dds_cli.project_creator
|
|
36
|
-
import dds_cli.auth
|
|
37
|
-
import dds_cli.project_status
|
|
38
36
|
import dds_cli.project_info
|
|
37
|
+
import dds_cli.project_status
|
|
38
|
+
import dds_cli.superadmin_helper
|
|
39
|
+
import dds_cli.unit_manager
|
|
39
40
|
import dds_cli.user
|
|
40
41
|
import dds_cli.utils
|
|
41
|
-
import dds_cli.message_helper
|
|
42
42
|
from dds_cli.options import (
|
|
43
|
+
break_on_fail_flag,
|
|
43
44
|
destination_option,
|
|
44
45
|
email_arg,
|
|
45
46
|
email_option,
|
|
46
47
|
folder_option,
|
|
48
|
+
json_flag,
|
|
49
|
+
nomail_flag,
|
|
47
50
|
num_threads_option,
|
|
48
51
|
project_option,
|
|
52
|
+
silent_flag,
|
|
53
|
+
size_flag,
|
|
49
54
|
sort_projects_option,
|
|
50
55
|
source_option,
|
|
51
56
|
source_path_file_option,
|
|
52
57
|
token_path_option,
|
|
53
|
-
username_option,
|
|
54
|
-
break_on_fail_flag,
|
|
55
|
-
json_flag,
|
|
56
|
-
nomail_flag,
|
|
57
|
-
silent_flag,
|
|
58
|
-
size_flag,
|
|
59
58
|
tree_flag,
|
|
60
59
|
usage_flag,
|
|
60
|
+
username_option,
|
|
61
61
|
users_flag,
|
|
62
62
|
)
|
|
63
63
|
|
|
64
|
-
# import dds_cli.dds_gui.app
|
|
65
|
-
|
|
66
64
|
####################################################################################################
|
|
67
65
|
# START LOGGING CONFIG ###################################################### START LOGGING CONFIG #
|
|
68
66
|
####################################################################################################
|
|
@@ -201,27 +199,11 @@ def dds_main(click_ctx, verbose, force_no_log, log_file, no_prompt, token_path):
|
|
|
201
199
|
else:
|
|
202
200
|
file_handler = dds_cli.utils.setup_logging_to_file(filename=log_file)
|
|
203
201
|
LOG.addHandler(file_handler)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
click_ctx.obj.update({"DEFAULT_LOG": False})
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
### GUI COMMAND ###
|
|
213
|
-
|
|
214
|
-
# TODO: Should totp be passed to the gui?
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# @dds_main.command(name="gui")
|
|
218
|
-
# @click.pass_obj
|
|
219
|
-
# def gui(click_ctx):
|
|
220
|
-
# """Start the DDS GUI."""
|
|
221
|
-
# gui_app = dds_cli.dds_gui.app.DDSApp(token_path=click_ctx.get("TOKEN_PATH"))
|
|
222
|
-
# gui_app.title = "SciLifeLab Data Delivery System"
|
|
223
|
-
# gui_app.sub_title = "CLI Version: " + dds_cli.__version__
|
|
224
|
-
# gui_app.run()
|
|
202
|
+
elif force_no_log:
|
|
203
|
+
LOG.warning(
|
|
204
|
+
"You have chosen to turn off the recommended default logging with the '--force-no-log' option."
|
|
205
|
+
)
|
|
206
|
+
click_ctx.obj.update({"DEFAULT_LOG": False})
|
|
225
207
|
|
|
226
208
|
|
|
227
209
|
# ************************************************************************************************ #
|
|
@@ -285,29 +267,27 @@ def list_projects_and_contents(
|
|
|
285
267
|
projects = lister.list_projects(sort_by=sort, show_all=show_all)
|
|
286
268
|
if json:
|
|
287
269
|
dds_cli.utils.console.print_json(data=projects)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
LOG.debug("No project entered, exiting.")
|
|
310
|
-
break
|
|
270
|
+
# If an interactive terminal, ask user if they want to view files for a project
|
|
271
|
+
elif sys.stdout.isatty() and not lister.no_prompt:
|
|
272
|
+
project_ids = [p["Project ID"] for p in projects]
|
|
273
|
+
LOG.info(
|
|
274
|
+
"Would you like to view files in a specific project? Leave blank to exit."
|
|
275
|
+
)
|
|
276
|
+
# Keep asking until we get a valid response
|
|
277
|
+
while project not in project_ids:
|
|
278
|
+
try:
|
|
279
|
+
project = questionary.autocomplete(
|
|
280
|
+
"Project ID:",
|
|
281
|
+
choices=project_ids,
|
|
282
|
+
validate=lambda x: x in project_ids or x == "",
|
|
283
|
+
style=dds_cli.dds_questionary_styles,
|
|
284
|
+
).unsafe_ask()
|
|
285
|
+
assert project and project != ""
|
|
286
|
+
|
|
287
|
+
# If didn't enter anything, convert to None and exit
|
|
288
|
+
except (KeyboardInterrupt, AssertionError):
|
|
289
|
+
LOG.debug("No project entered, exiting.")
|
|
290
|
+
break
|
|
311
291
|
|
|
312
292
|
# List all files in a project if we know a project ID
|
|
313
293
|
if project:
|
|
@@ -383,6 +363,7 @@ def list_projects_and_contents(
|
|
|
383
363
|
dds_cli.exceptions.AuthenticationError,
|
|
384
364
|
dds_cli.exceptions.ApiResponseError,
|
|
385
365
|
dds_cli.exceptions.ApiRequestError,
|
|
366
|
+
dds_cli.exceptions.DDSCLIException,
|
|
386
367
|
) as err:
|
|
387
368
|
LOG.error(err)
|
|
388
369
|
sys.exit(1)
|
|
@@ -544,13 +525,14 @@ def configure():
|
|
|
544
525
|
"Which method would you like to use?", choices=["Email", "Authenticator App", "Cancel"]
|
|
545
526
|
).ask()
|
|
546
527
|
|
|
528
|
+
auth_method: str | None = None # type hint, initialized
|
|
547
529
|
if auth_method_choice == "Cancel":
|
|
548
530
|
LOG.info("Two-factor authentication method not configured.")
|
|
549
531
|
sys.exit(0)
|
|
550
532
|
elif auth_method_choice == "Authenticator App":
|
|
551
|
-
auth_method
|
|
533
|
+
auth_method = "totp"
|
|
552
534
|
elif auth_method_choice == "Email":
|
|
553
|
-
auth_method
|
|
535
|
+
auth_method = "hotp"
|
|
554
536
|
|
|
555
537
|
with dds_cli.auth.Auth(authenticate=True, force_renew_token=False) as authenticator:
|
|
556
538
|
authenticator.twofactor(auth_method=auth_method)
|
|
@@ -797,15 +779,14 @@ def delete_user(click_ctx, email, self, is_invite):
|
|
|
797
779
|
proceed_deletion = rich.prompt.Confirm.ask(
|
|
798
780
|
f"Delete invitation of {email} to Data Delivery System?"
|
|
799
781
|
)
|
|
782
|
+
elif self:
|
|
783
|
+
proceed_deletion = rich.prompt.Confirm.ask(
|
|
784
|
+
"Are you sure? Deleted accounts can't be restored!"
|
|
785
|
+
)
|
|
800
786
|
else:
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
)
|
|
805
|
-
else:
|
|
806
|
-
proceed_deletion = rich.prompt.Confirm.ask(
|
|
807
|
-
f"Delete Data Delivery System user account associated with {email}"
|
|
808
|
-
)
|
|
787
|
+
proceed_deletion = rich.prompt.Confirm.ask(
|
|
788
|
+
f"Delete Data Delivery System user account associated with {email}"
|
|
789
|
+
)
|
|
809
790
|
|
|
810
791
|
if proceed_deletion:
|
|
811
792
|
try:
|
|
@@ -885,6 +866,8 @@ def activate_user(click_ctx, email):
|
|
|
885
866
|
Super Admins: All users
|
|
886
867
|
Unit Admins: Unit Admins / Personnel
|
|
887
868
|
"""
|
|
869
|
+
proceed_activation = False # default assignment
|
|
870
|
+
|
|
888
871
|
if click_ctx.get("NO_PROMPT", False):
|
|
889
872
|
pass
|
|
890
873
|
else:
|
|
@@ -925,6 +908,8 @@ def deactivate_user(click_ctx, email):
|
|
|
925
908
|
Super Admins: All users
|
|
926
909
|
Unit Admins: Unit Admins / Personnel
|
|
927
910
|
"""
|
|
911
|
+
proceed_deactivation = False # default assignment
|
|
912
|
+
|
|
928
913
|
if click_ctx.get("NO_PROMPT", False):
|
|
929
914
|
pass
|
|
930
915
|
else:
|
|
@@ -1166,6 +1151,13 @@ def display_project_status(click_ctx, project, show_history):
|
|
|
1166
1151
|
sys.exit(1)
|
|
1167
1152
|
|
|
1168
1153
|
|
|
1154
|
+
def validate_deadline(_ctx, _param, value):
|
|
1155
|
+
"""Validate that the deadline is a positive number of days between 1 and 90."""
|
|
1156
|
+
if value is not None and value not in range(1, 91):
|
|
1157
|
+
raise click.BadParameter("Deadline must be a positive number of days between 1 and 90.")
|
|
1158
|
+
return value
|
|
1159
|
+
|
|
1160
|
+
|
|
1169
1161
|
# -- dds project status release -- #
|
|
1170
1162
|
@project_status.command(name="release", no_args_is_help=True)
|
|
1171
1163
|
# Options
|
|
@@ -1174,7 +1166,8 @@ def display_project_status(click_ctx, project, show_history):
|
|
|
1174
1166
|
"--deadline",
|
|
1175
1167
|
required=False,
|
|
1176
1168
|
type=int,
|
|
1177
|
-
|
|
1169
|
+
callback=validate_deadline,
|
|
1170
|
+
help="Deadline in days when releasing a project. Must be a positive number of days (maximum 90 days).",
|
|
1178
1171
|
)
|
|
1179
1172
|
@nomail_flag(help_message="Do not send e-mail notifications regarding project updates.")
|
|
1180
1173
|
@click.pass_obj
|
|
@@ -1315,7 +1308,8 @@ def delete_project(click_ctx, project: str):
|
|
|
1315
1308
|
"--new-deadline",
|
|
1316
1309
|
required=False,
|
|
1317
1310
|
type=int,
|
|
1318
|
-
|
|
1311
|
+
callback=validate_deadline,
|
|
1312
|
+
help="Number of days to extend the deadline. Must be a positive number of days (maximum 90 days).",
|
|
1319
1313
|
)
|
|
1320
1314
|
@click.pass_obj
|
|
1321
1315
|
def extend_deadline(click_ctx, project: str, new_deadline: int):
|
|
@@ -1631,7 +1625,9 @@ def data_group_command(_):
|
|
|
1631
1625
|
"--mount-dir",
|
|
1632
1626
|
"-md",
|
|
1633
1627
|
required=False,
|
|
1634
|
-
type=
|
|
1628
|
+
type=click.Path(
|
|
1629
|
+
exists=False, file_okay=False, dir_okay=True, resolve_path=True, path_type=pathlib.Path
|
|
1630
|
+
),
|
|
1635
1631
|
help=(
|
|
1636
1632
|
"New directory where the files will be mounted before upload "
|
|
1637
1633
|
"and any error log files will be saved for a specific upload."
|
|
@@ -1739,6 +1735,7 @@ def put_data(
|
|
|
1739
1735
|
dds_cli.exceptions.ApiRequestError,
|
|
1740
1736
|
dds_cli.exceptions.NoKeyError,
|
|
1741
1737
|
dds_cli.exceptions.NoDataError,
|
|
1738
|
+
dds_cli.exceptions.DDSCLIException,
|
|
1742
1739
|
) as err:
|
|
1743
1740
|
LOG.error(err)
|
|
1744
1741
|
sys.exit(1)
|
|
@@ -1753,7 +1750,9 @@ def put_data(
|
|
|
1753
1750
|
@source_path_file_option()
|
|
1754
1751
|
@destination_option(
|
|
1755
1752
|
help_message="Destination of downloaded data.",
|
|
1756
|
-
option_type=
|
|
1753
|
+
option_type=click.Path(
|
|
1754
|
+
exists=False, file_okay=False, dir_okay=True, resolve_path=True, path_type=pathlib.Path
|
|
1755
|
+
),
|
|
1757
1756
|
)
|
|
1758
1757
|
# Flags
|
|
1759
1758
|
@break_on_fail_flag(help_message="Cancel download of all files if one fails.")
|
|
@@ -2020,12 +2019,11 @@ def rm_data(click_ctx, project, file, folder, rm_all):
|
|
|
2020
2019
|
if rm_all:
|
|
2021
2020
|
if no_prompt:
|
|
2022
2021
|
LOG.warning("Deleting all files within project '%s'", project)
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
)
|
|
2027
|
-
|
|
2028
|
-
sys.exit(0)
|
|
2022
|
+
elif not rich.prompt.Confirm.ask(
|
|
2023
|
+
f"Are you sure you want to delete all files within project '{project}'?"
|
|
2024
|
+
):
|
|
2025
|
+
LOG.info("Probably for the best. Exiting.")
|
|
2026
|
+
sys.exit(0)
|
|
2029
2027
|
|
|
2030
2028
|
try:
|
|
2031
2029
|
with dds_cli.data_remover.DataRemover(
|
|
@@ -2269,6 +2267,7 @@ def get_stats(click_ctx):
|
|
|
2269
2267
|
dds_cli.exceptions.AuthenticationError,
|
|
2270
2268
|
dds_cli.exceptions.ApiResponseError,
|
|
2271
2269
|
dds_cli.exceptions.ApiRequestError,
|
|
2270
|
+
dds_cli.exceptions.DDSCLIException,
|
|
2272
2271
|
) as err:
|
|
2273
2272
|
LOG.error(err)
|
|
2274
2273
|
sys.exit(1)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Shared constants used in the DDS CLI.
|
|
2
|
+
|
|
3
|
+
Using this will avoid e.g. circular imports and
|
|
4
|
+
make it easier to maintain.
|
|
5
|
+
|
|
6
|
+
TODO: Move other constants here from __init__.py.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Timeout settings for upload and download
|
|
10
|
+
READ_TIMEOUT = 300
|
|
11
|
+
CONNECT_TIMEOUT = 60
|
|
12
|
+
|
|
13
|
+
# Retry settings for download
|
|
14
|
+
DOWNLOAD_MAX_RETRIES = 5
|
|
15
|
+
DOWNLOAD_BACKOFF_FACTOR = 2
|
|
16
|
+
DOWNLOAD_INITIAL_WAIT = 1 # seconds
|
|
17
|
+
|
|
18
|
+
# Import these constants when using '*'
|
|
19
|
+
__all__ = [
|
|
20
|
+
"READ_TIMEOUT",
|
|
21
|
+
"CONNECT_TIMEOUT",
|
|
22
|
+
"DOWNLOAD_MAX_RETRIES",
|
|
23
|
+
"DOWNLOAD_BACKOFF_FACTOR",
|
|
24
|
+
"DOWNLOAD_INITIAL_WAIT",
|
|
25
|
+
]
|
|
@@ -154,6 +154,7 @@ def removal_spinner(func):
|
|
|
154
154
|
SpinnerColumn(spinner_name="dots12", style="white"),
|
|
155
155
|
console=dds_cli.utils.stderr_console,
|
|
156
156
|
) as progress:
|
|
157
|
+
description: str | None = None # type hint, initialized
|
|
157
158
|
# Determine spinner text
|
|
158
159
|
if func.__name__ == "remove_all":
|
|
159
160
|
description = f"Removing all files in project {self.project}"
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
# Standard library
|
|
8
8
|
import logging
|
|
9
9
|
import pathlib
|
|
10
|
+
import time
|
|
10
11
|
|
|
11
12
|
# Installed
|
|
12
13
|
import requests
|
|
@@ -101,8 +102,17 @@ class DataGetter(base.DDSBaseClass):
|
|
|
101
102
|
|
|
102
103
|
if not self.filehandler.data:
|
|
103
104
|
if self.temporary_directory and self.temporary_directory.is_dir():
|
|
104
|
-
LOG.debug("Deleting
|
|
105
|
-
|
|
105
|
+
LOG.debug("Deleting staging directory '%s'.", self.temporary_directory)
|
|
106
|
+
try:
|
|
107
|
+
dds_cli.utils.delete_folder(self.temporary_directory)
|
|
108
|
+
except OSError as err:
|
|
109
|
+
# Folder deletion may fail if log file is still being written to
|
|
110
|
+
# This is not critical - the important thing is to show the error message
|
|
111
|
+
LOG.error(
|
|
112
|
+
"Could not delete staging directory %s: %s",
|
|
113
|
+
self.temporary_directory,
|
|
114
|
+
err,
|
|
115
|
+
)
|
|
106
116
|
raise dds_cli.exceptions.DownloadError("No files to download.")
|
|
107
117
|
|
|
108
118
|
self.status = self.filehandler.create_download_status_dict()
|
|
@@ -234,35 +244,68 @@ class DataGetter(base.DDSBaseClass):
|
|
|
234
244
|
error = ""
|
|
235
245
|
file_local = self.filehandler.data[file]["path_downloaded"]
|
|
236
246
|
file_remote = self.filehandler.data[file]["url"]
|
|
247
|
+
file_name_in_db = escape(str(self.filehandler.data[file]["name_in_db"]))
|
|
237
248
|
|
|
238
|
-
|
|
239
|
-
with requests.get(
|
|
240
|
-
file_remote,
|
|
241
|
-
stream=True,
|
|
242
|
-
timeout=(constants.CONNECT_TIMEOUT, constants.READ_TIMEOUT),
|
|
243
|
-
) as req:
|
|
244
|
-
req.raise_for_status()
|
|
245
|
-
with file_local.open(mode="wb") as new_file:
|
|
246
|
-
for chunk in req.iter_content(chunk_size=FileSegment.SEGMENT_SIZE_CIPHER):
|
|
247
|
-
progress.update(task, advance=len(chunk))
|
|
248
|
-
new_file.write(chunk)
|
|
249
|
-
except (
|
|
249
|
+
retryable_exceptions = (
|
|
250
250
|
requests.exceptions.ConnectTimeout,
|
|
251
|
-
requests.exceptions.HTTPError,
|
|
252
251
|
requests.exceptions.ReadTimeout,
|
|
253
252
|
requests.exceptions.Timeout,
|
|
254
253
|
requests.exceptions.ConnectionError,
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
max_retries = constants.DOWNLOAD_MAX_RETRIES
|
|
257
|
+
backoff_factor = constants.DOWNLOAD_BACKOFF_FACTOR
|
|
258
|
+
wait = constants.DOWNLOAD_INITIAL_WAIT
|
|
259
|
+
retry_messages = []
|
|
260
|
+
|
|
261
|
+
for attempt in range(1, max_retries + 1):
|
|
262
|
+
error = ""
|
|
263
|
+
if attempt > 1:
|
|
264
|
+
progress.reset(task, completed=0)
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
with requests.get(
|
|
268
|
+
file_remote,
|
|
269
|
+
stream=True,
|
|
270
|
+
timeout=(constants.CONNECT_TIMEOUT, constants.READ_TIMEOUT),
|
|
271
|
+
) as req:
|
|
272
|
+
req.raise_for_status()
|
|
273
|
+
with file_local.open(mode="wb") as new_file:
|
|
274
|
+
for chunk in req.iter_content(chunk_size=FileSegment.SEGMENT_SIZE_CIPHER):
|
|
275
|
+
progress.update(task, advance=len(chunk))
|
|
276
|
+
new_file.write(chunk)
|
|
277
|
+
except (requests.exceptions.HTTPError, *retryable_exceptions) as err:
|
|
278
|
+
if (
|
|
279
|
+
isinstance(err, requests.exceptions.HTTPError)
|
|
280
|
+
and hasattr(err, "response")
|
|
281
|
+
and hasattr(err.response, "status_code")
|
|
282
|
+
and err.response.status_code == 404
|
|
283
|
+
):
|
|
284
|
+
error = "File not found! Please contact support."
|
|
285
|
+
break
|
|
263
286
|
error = str(err)
|
|
264
|
-
|
|
265
|
-
|
|
287
|
+
if attempt < max_retries:
|
|
288
|
+
retry_msg = (
|
|
289
|
+
f"Download attempt {attempt}/{max_retries} failed for "
|
|
290
|
+
f"'{file_name_in_db}': {error}. Retrying in {wait}s..."
|
|
291
|
+
)
|
|
292
|
+
retry_messages.append(retry_msg)
|
|
293
|
+
LOG.warning(retry_msg)
|
|
294
|
+
time.sleep(wait)
|
|
295
|
+
wait *= backoff_factor
|
|
296
|
+
else:
|
|
297
|
+
downloaded = True
|
|
298
|
+
break
|
|
299
|
+
|
|
300
|
+
if not downloaded and error:
|
|
301
|
+
if retry_messages:
|
|
302
|
+
error = " | ".join(retry_messages) + f" | Final error: {error}"
|
|
303
|
+
LOG.error(
|
|
304
|
+
"Download failed for '%s' after %d attempts: %s",
|
|
305
|
+
file_name_in_db,
|
|
306
|
+
max_retries,
|
|
307
|
+
error,
|
|
308
|
+
)
|
|
266
309
|
|
|
267
310
|
return downloaded, error
|
|
268
311
|
|
|
@@ -5,26 +5,24 @@
|
|
|
5
5
|
###############################################################################
|
|
6
6
|
|
|
7
7
|
# Standard library
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
import logging
|
|
10
|
-
from typing import Tuple, Union, List
|
|
11
8
|
import datetime
|
|
9
|
+
import logging
|
|
12
10
|
import pathlib
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import List, Tuple, Union
|
|
13
13
|
|
|
14
14
|
# Installed
|
|
15
|
-
|
|
15
|
+
import pytz
|
|
16
|
+
import tzlocal
|
|
16
17
|
from rich.markup import escape
|
|
18
|
+
from rich.padding import Padding
|
|
17
19
|
from rich.table import Table
|
|
18
20
|
from rich.tree import Tree
|
|
19
|
-
import pytz
|
|
20
|
-
import tzlocal
|
|
21
21
|
|
|
22
22
|
# Own modules
|
|
23
|
-
from dds_cli import base
|
|
24
|
-
from dds_cli import exceptions
|
|
25
|
-
import dds_cli.utils
|
|
26
|
-
from dds_cli import DDSEndpoint
|
|
23
|
+
from dds_cli import DDSEndpoint, base, exceptions
|
|
27
24
|
from dds_cli import text_handler as th
|
|
25
|
+
import dds_cli.utils
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
###############################################################################
|
|
@@ -235,7 +233,7 @@ class DataLister(base.DDSBaseClass):
|
|
|
235
233
|
error_message="Failed to list the project's directory tree",
|
|
236
234
|
)
|
|
237
235
|
|
|
238
|
-
if
|
|
236
|
+
if "files_folders" not in resp_json:
|
|
239
237
|
raise exceptions.NoDataError(f"Could not find folder: '{folder}'")
|
|
240
238
|
|
|
241
239
|
sorted_files_folders = sorted(resp_json["files_folders"], key=lambda f: f["name"])
|
|
@@ -268,7 +266,11 @@ class DataLister(base.DDSBaseClass):
|
|
|
268
266
|
# Get max length of size string
|
|
269
267
|
max_size = max(
|
|
270
268
|
(
|
|
271
|
-
len(
|
|
269
|
+
len(
|
|
270
|
+
dds_cli.utils.format_api_response(
|
|
271
|
+
response=x["size"], key="Size", binary=self.binary
|
|
272
|
+
).split(" ", maxsplit=1)[0]
|
|
273
|
+
)
|
|
272
274
|
for x in sorted_files_folders
|
|
273
275
|
if show_size and "size" in x
|
|
274
276
|
),
|
|
@@ -280,9 +282,12 @@ class DataLister(base.DDSBaseClass):
|
|
|
280
282
|
is_folder = item.pop("folder")
|
|
281
283
|
|
|
282
284
|
if not is_folder:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
285
|
+
formatted_size = None
|
|
286
|
+
if show_size and "size" in item:
|
|
287
|
+
formatted_size = dds_cli.utils.format_api_response(
|
|
288
|
+
response=item["size"], key="Size", binary=self.binary
|
|
289
|
+
)
|
|
290
|
+
tree.subtrees.append((escape(item["name"]), formatted_size))
|
|
286
291
|
else:
|
|
287
292
|
subtree, _max_string, _max_size = __construct_file_tree(
|
|
288
293
|
pathlib.Path(folder, item["name"]).as_posix() if folder else item["name"],
|
|
@@ -348,15 +353,7 @@ class DataLister(base.DDSBaseClass):
|
|
|
348
353
|
string_len=len(node[0]),
|
|
349
354
|
max_string_len=max_str - 4 * depth,
|
|
350
355
|
)
|
|
351
|
-
line += f"{tab}{node[1].split()[0]}"
|
|
352
|
-
|
|
353
|
-
# Define space between number and size format
|
|
354
|
-
tabs_bf_format = th.TextHandler.format_tabs(
|
|
355
|
-
string_len=len(node[1].split()[1]),
|
|
356
|
-
max_string_len=max_size,
|
|
357
|
-
tab_len=2,
|
|
358
|
-
)
|
|
359
|
-
line += f"{tabs_bf_format}{node[1].split()[1]}"
|
|
356
|
+
line += f"{tab}{node[1].split()[0]} {node[1].split()[1]}"
|
|
360
357
|
tree.add(line)
|
|
361
358
|
|
|
362
359
|
return tree, tree_length
|