dds-cli 2.12.0__tar.gz → 2.13.0__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.12.0 → dds_cli-2.13.0}/PKG-INFO +3 -3
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/__init__.py +1 -1
- dds_cli-2.13.0/dds_cli/constants.py +14 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/data_getter.py +6 -2
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/app.py +2 -2
- dds_cli-2.13.0/dds_cli/dds_gui/dds_state_manager.py +165 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/authentication_form.py +21 -5
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view.py +7 -10
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/project_status.py +1 -1
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/s3_connector.py +9 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/user.py +4 -4
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/version.py +1 -1
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/PKG-INFO +3 -3
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/SOURCES.txt +7 -5
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/requires.txt +2 -2
- {dds_cli-2.12.0 → dds_cli-2.13.0}/gui_build/gui_standalone.py +3 -1
- dds_cli-2.13.0/tests/gui_tests/test_authentication.py +1567 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/gui_tests/test_important_information.py +13 -10
- dds_cli-2.13.0/tests/gui_tests/test_project_content.py +1000 -0
- dds_cli-2.13.0/tests/gui_tests/test_project_information.py +650 -0
- dds_cli-2.13.0/tests/gui_tests/test_project_list.py +558 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_auth.py +5 -3
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_base.py +9 -7
- dds_cli-2.13.0/tests/test_data_getter.py +133 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_data_remover.py +17 -20
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_file_compressor.py +17 -25
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_file_encryptor.py +1 -2
- dds_cli-2.13.0/tests/test_file_handler_local.py +104 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_motd_manager.py +15 -13
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_project_status.py +19 -28
- dds_cli-2.13.0/tests/test_s3_connector.py +112 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_superadmin_helper.py +3 -3
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_user.py +97 -20
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_utils.py +26 -16
- dds_cli-2.12.0/dds_cli/dds_gui/components/dds_tree_view.py +0 -55
- dds_cli-2.12.0/dds_cli/dds_gui/dds_state_manager.py +0 -202
- dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_content.py +0 -31
- dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_information.py +0 -99
- dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_list.py +0 -37
- dds_cli-2.12.0/tests/gui_tests/test_authentication.py +0 -244
- dds_cli-2.12.0/tests/test_file_handler_local.py +0 -90
- {dds_cli-2.12.0 → dds_cli-2.13.0}/LICENSE +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/README.md +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/__main__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/account_manager.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/auth.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/base.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/custom_decorators.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/data_lister.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/data_putter.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/data_remover.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_button.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_container.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_footer.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_form.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_input.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_modal.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_select.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_status_chip.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_text_item.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/authentication.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/modals/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/modals/login_modal.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/modals/logout_modal.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/modals/reauthenticate_modal.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view_mode/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view_mode/project_actions.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/download_data.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/user_access.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/types/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/types/dds_severity_types.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/types/dds_status_types.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/directory.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/exceptions.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_compressor.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_encryptor.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_handler.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_handler_local.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_handler_remote.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/message_helper.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/motd_manager.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/options.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/project_creator.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/project_info.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/status.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/superadmin_helper.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/text_handler.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/timestamp.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/unit_manager.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/utils.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/dependency_links.txt +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/entry_points.txt +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/not-zip-safe +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/top_level.txt +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/gui_build/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/pyproject.toml +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/setup.cfg +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/setup.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/gui_tests/__init__.py +0 -0
- {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_account_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dds_cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.0
|
|
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
|
|
@@ -18,7 +18,7 @@ Requires-Dist: boto3==1.24.73
|
|
|
18
18
|
Requires-Dist: botocore==1.27.73
|
|
19
19
|
Requires-Dist: click==8.1.3
|
|
20
20
|
Requires-Dist: click-pathlib==2020.3.13.0
|
|
21
|
-
Requires-Dist: cryptography==
|
|
21
|
+
Requires-Dist: cryptography==44.0.1
|
|
22
22
|
Requires-Dist: immutabledict==2.2.1
|
|
23
23
|
Requires-Dist: jwcrypto==1.5.6
|
|
24
24
|
Requires-Dist: prettytable==3.7.0
|
|
@@ -27,7 +27,7 @@ Requires-Dist: PyNaCl==1.5.0
|
|
|
27
27
|
Requires-Dist: pytz==2022.2.1
|
|
28
28
|
Requires-Dist: PyYAML==6.0.2
|
|
29
29
|
Requires-Dist: questionary==1.10.0
|
|
30
|
-
Requires-Dist: requests==2.32.
|
|
30
|
+
Requires-Dist: requests==2.32.4
|
|
31
31
|
Requires-Dist: rich==13.6.0
|
|
32
32
|
Requires-Dist: rich-click==1.5.2
|
|
33
33
|
Requires-Dist: simplejson==3.17.6
|
|
@@ -44,7 +44,7 @@ DDS_DIR_REQUIRED_METHODS = ["put", "get"]
|
|
|
44
44
|
DDS_KEYS_REQUIRED_METHODS = ["put", "get"]
|
|
45
45
|
|
|
46
46
|
# Token related variables
|
|
47
|
-
TOKEN_FILE = pathlib.Path
|
|
47
|
+
TOKEN_FILE = pathlib.Path.home() / ".dds_cli_token"
|
|
48
48
|
TOKEN_EXPIRATION_WARNING_THRESHOLD = datetime.timedelta(hours=6)
|
|
49
49
|
|
|
50
50
|
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
# Import these constants when using '*'
|
|
14
|
+
__all__ = ["READ_TIMEOUT", "CONNECT_TIMEOUT"]
|
|
@@ -14,6 +14,7 @@ from rich.markup import escape
|
|
|
14
14
|
from rich.progress import Progress, SpinnerColumn
|
|
15
15
|
|
|
16
16
|
# Own modules
|
|
17
|
+
from dds_cli import constants
|
|
17
18
|
from dds_cli import DDSEndpoint, FileSegment
|
|
18
19
|
from dds_cli import file_handler_remote as fhr
|
|
19
20
|
from dds_cli import data_remover as dr
|
|
@@ -235,8 +236,11 @@ class DataGetter(base.DDSBaseClass):
|
|
|
235
236
|
file_remote = self.filehandler.data[file]["url"]
|
|
236
237
|
|
|
237
238
|
try:
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
with requests.get(
|
|
240
|
+
file_remote,
|
|
241
|
+
stream=True,
|
|
242
|
+
timeout=(constants.CONNECT_TIMEOUT, constants.READ_TIMEOUT),
|
|
243
|
+
) as req:
|
|
240
244
|
req.raise_for_status()
|
|
241
245
|
with file_local.open(mode="wb") as new_file:
|
|
242
246
|
for chunk in req.iter_content(chunk_size=FileSegment.SEGMENT_SIZE_CIPHER):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""GUI Application for DDS CLI."""
|
|
2
2
|
|
|
3
|
-
from textual.app import
|
|
3
|
+
from textual.app import ComposeResult
|
|
4
4
|
from textual.binding import Binding
|
|
5
5
|
from textual.widgets import Header
|
|
6
6
|
from textual.theme import Theme
|
|
@@ -33,7 +33,7 @@ theme = Theme(
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class DDSApp(
|
|
36
|
+
class DDSApp(DDSStateManager): ### Moved Textual App class to State Manager to access notifications
|
|
37
37
|
"""Textual App for DDS CLI."""
|
|
38
38
|
|
|
39
39
|
def __init__(self, token_path: str):
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""DDS State Manager"""
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
from typing import List
|
|
5
|
+
from textual.app import App
|
|
6
|
+
from textual import work
|
|
7
|
+
from textual.reactive import reactive
|
|
8
|
+
|
|
9
|
+
import dds_cli.auth
|
|
10
|
+
import dds_cli.data_lister
|
|
11
|
+
import dds_cli.exceptions
|
|
12
|
+
import dds_cli.project_info
|
|
13
|
+
|
|
14
|
+
from dds_cli.dds_gui.models.project import ProjectContentData
|
|
15
|
+
from dds_cli.dds_gui.models.project_information import ProjectInformationData
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DDSStateManager(App):
|
|
19
|
+
"""
|
|
20
|
+
State manager for the DDS CLI. Consists of reactive states available app wide.
|
|
21
|
+
|
|
22
|
+
Reactive attributes are used to update the UI, fetch data, set new states, etc.
|
|
23
|
+
Reactive attributes are recomposed when the state changes,
|
|
24
|
+
triggering re-renders and re-computations of the derived states.
|
|
25
|
+
|
|
26
|
+
Derived states are attributes computed based on the reactive attributes.
|
|
27
|
+
When the reactive attributes are updated, the derived states are automatically updated.
|
|
28
|
+
|
|
29
|
+
Setters are used to avoid pylint warnings about attribute-defined-outside-init.
|
|
30
|
+
Functionally, the reactive attributes can be set in the child classes,
|
|
31
|
+
but are set here for consistence over the app, instead of ignoring the pylint warnings.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
#### TOKEN PATH #########################################################
|
|
35
|
+
|
|
36
|
+
# Default token path for CLI authentication token
|
|
37
|
+
token_path = str(pathlib.Path.home() / ".dds_cli_token")
|
|
38
|
+
|
|
39
|
+
#### AUTH ################################################################
|
|
40
|
+
|
|
41
|
+
auth: reactive[dds_cli.auth.Auth] = reactive(
|
|
42
|
+
dds_cli.auth.Auth(authenticate=False, token_path=token_path), recompose=True
|
|
43
|
+
)
|
|
44
|
+
auth_status: reactive[bool] = reactive(False, recompose=True)
|
|
45
|
+
|
|
46
|
+
def set_auth_status(self, new_auth_status: bool) -> None:
|
|
47
|
+
"""Set the auth status."""
|
|
48
|
+
self.auth_status = new_auth_status
|
|
49
|
+
|
|
50
|
+
#### PROJECT LISTING ####################################################
|
|
51
|
+
|
|
52
|
+
project_list: reactive[List[dict]] = reactive(None, recompose=True)
|
|
53
|
+
selected_project_id: reactive[str] = reactive(None, recompose=True)
|
|
54
|
+
|
|
55
|
+
def fetch_projects(self) -> List[str]:
|
|
56
|
+
"""Fetch the projects and automatically compute project_ids via reactive watcher."""
|
|
57
|
+
self.project_list: List[dict] = dds_cli.data_lister.DataLister(json=True).list_projects()
|
|
58
|
+
|
|
59
|
+
def set_selected_project_id(self, project_id: str) -> None:
|
|
60
|
+
"""Set the selected project id."""
|
|
61
|
+
self.selected_project_id = project_id
|
|
62
|
+
|
|
63
|
+
#### PROJECT CONTENT #####################################################
|
|
64
|
+
|
|
65
|
+
project_content: reactive[ProjectContentData] = reactive(None, recompose=True)
|
|
66
|
+
is_loading: reactive[bool] = reactive(False, recompose=True)
|
|
67
|
+
|
|
68
|
+
@work(exclusive=True, thread=True)
|
|
69
|
+
def load_project_content(self, project_id: str) -> None:
|
|
70
|
+
"""Background worker to fetch project content for a project id.
|
|
71
|
+
This solution was proposed to avoid unnecessary re-renders of the project
|
|
72
|
+
content widget while still keeping the label reactive.
|
|
73
|
+
Reference: https://textual.textualize.io/guide/workers/
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
project_content = dds_cli.data_lister.DataLister(
|
|
77
|
+
json=True, project=project_id
|
|
78
|
+
).list_recursive()
|
|
79
|
+
except (
|
|
80
|
+
dds_cli.exceptions.ApiRequestError,
|
|
81
|
+
dds_cli.exceptions.ApiResponseError,
|
|
82
|
+
dds_cli.exceptions.DDSCLIException,
|
|
83
|
+
) as err:
|
|
84
|
+
self.call_from_thread(self._on_project_content_error, project_id, str(err), "error")
|
|
85
|
+
return
|
|
86
|
+
except dds_cli.exceptions.NoDataError as data_err:
|
|
87
|
+
self.call_from_thread(
|
|
88
|
+
self._on_project_content_error, project_id, str(data_err), "warning"
|
|
89
|
+
)
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
content = ProjectContentData.from_dict(project_content, project_name=project_id)
|
|
93
|
+
self.call_from_thread(self._on_project_content_loaded, content)
|
|
94
|
+
|
|
95
|
+
def _on_project_content_loaded(self, content: ProjectContentData) -> None:
|
|
96
|
+
"""Handle successful content load on the main thread."""
|
|
97
|
+
self.is_loading = False
|
|
98
|
+
self.project_content = content
|
|
99
|
+
|
|
100
|
+
def _on_project_content_error(self, project_id: str, message: str, severity: str) -> None:
|
|
101
|
+
"""Handle content load error on the main thread."""
|
|
102
|
+
self.is_loading = False
|
|
103
|
+
if severity == "warning":
|
|
104
|
+
self.notify(f"No data found for project {project_id}: {message}", severity="warning")
|
|
105
|
+
else:
|
|
106
|
+
self.notify(f"Failed to fetch project content: {message}", severity="error")
|
|
107
|
+
if self.selected_project_id == project_id:
|
|
108
|
+
self.project_content = None
|
|
109
|
+
|
|
110
|
+
#### PROJECT INFORMATION #################################################
|
|
111
|
+
|
|
112
|
+
project_information: reactive[ProjectInformationData] = reactive(None, recompose=True)
|
|
113
|
+
|
|
114
|
+
def fetch_project_information(self, project_id: str) -> None:
|
|
115
|
+
"""Fetch the project information for a project id."""
|
|
116
|
+
try:
|
|
117
|
+
self.project_information = ProjectInformationData.from_dict(
|
|
118
|
+
dds_cli.project_info.ProjectInfoManager(project=project_id).get_project_info()
|
|
119
|
+
)
|
|
120
|
+
except (
|
|
121
|
+
dds_cli.exceptions.ApiRequestError,
|
|
122
|
+
dds_cli.exceptions.ApiResponseError,
|
|
123
|
+
dds_cli.exceptions.DDSCLIException,
|
|
124
|
+
) as err:
|
|
125
|
+
self.notify(f"Failed to fetch project information: {err}", severity="error")
|
|
126
|
+
self.project_information = None
|
|
127
|
+
|
|
128
|
+
#### WATCHERS ###########################################################
|
|
129
|
+
|
|
130
|
+
def watch_auth_status(self, auth_status: bool) -> None:
|
|
131
|
+
"""Watch the auth status."""
|
|
132
|
+
if auth_status:
|
|
133
|
+
# Fetch the projects when the auth status is True.
|
|
134
|
+
# This is to ensure that the projects are fetched when the user is authenticated only.
|
|
135
|
+
# If called without auth status, recursion error occurs and/or the base class
|
|
136
|
+
# will try to authenticate in the CLI.
|
|
137
|
+
try:
|
|
138
|
+
self.fetch_projects()
|
|
139
|
+
except (
|
|
140
|
+
dds_cli.exceptions.ApiRequestError,
|
|
141
|
+
dds_cli.exceptions.ApiResponseError,
|
|
142
|
+
dds_cli.exceptions.DDSCLIException,
|
|
143
|
+
) as err:
|
|
144
|
+
self.notify(f"Failed to fetch projects: {err}", severity="error")
|
|
145
|
+
self.project_list = None # This triggers watch_projects to clear project_ids
|
|
146
|
+
else:
|
|
147
|
+
self.project_list = None # This triggers watch_projects to clear project_ids
|
|
148
|
+
self.selected_project_id = None
|
|
149
|
+
|
|
150
|
+
def watch_selected_project_id(self, selected_project_id: str) -> None:
|
|
151
|
+
"""Start loading project content when the selected project changes."""
|
|
152
|
+
# Clear current content and information when switching projects
|
|
153
|
+
self.project_information = None
|
|
154
|
+
self.project_content = None
|
|
155
|
+
|
|
156
|
+
if not selected_project_id:
|
|
157
|
+
self.is_loading = False
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
# Get project information
|
|
161
|
+
self.fetch_project_information(selected_project_id)
|
|
162
|
+
|
|
163
|
+
# Start loading project content
|
|
164
|
+
self.is_loading = True
|
|
165
|
+
self.load_project_content(selected_project_id)
|
{dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/authentication_form.py
RENAMED
|
@@ -48,8 +48,8 @@ class AuthenticationForm(Container):
|
|
|
48
48
|
def on_button_pressed(self, event: events.Click) -> None:
|
|
49
49
|
"""Handle button presses."""
|
|
50
50
|
if event.button.id == "send-2fa-code":
|
|
51
|
-
self.authenticate_user_credentials()
|
|
52
|
-
if
|
|
51
|
+
success = self.authenticate_user_credentials()
|
|
52
|
+
if success:
|
|
53
53
|
self.query_one("#dds-auth-form").remove()
|
|
54
54
|
self.query_one("#dds-auth-form-container").mount(
|
|
55
55
|
TwoFactorFormFields(id="dds-2fa-form")
|
|
@@ -57,25 +57,36 @@ class AuthenticationForm(Container):
|
|
|
57
57
|
if event.button.id == "login":
|
|
58
58
|
self.confirm_2factor_code()
|
|
59
59
|
|
|
60
|
-
def authenticate_user_credentials(self) ->
|
|
60
|
+
def authenticate_user_credentials(self) -> bool:
|
|
61
61
|
"""Authenticate the user credentials."""
|
|
62
62
|
username = self.query_one("#username").value
|
|
63
63
|
password = self.query_one("#password").value
|
|
64
64
|
|
|
65
|
+
if not username or not password:
|
|
66
|
+
self.notify("Error: Username and password are required.", severity="error", timeout=10)
|
|
67
|
+
return False
|
|
68
|
+
|
|
65
69
|
try:
|
|
66
70
|
self.partial_auth_token, self.secondfactor_method = self.auth.login(username, password)
|
|
67
71
|
self.notify("Two factor code sent to email.")
|
|
68
72
|
except (
|
|
69
73
|
dds_cli.exceptions.AuthenticationError,
|
|
70
74
|
dds_cli.exceptions.ApiRequestError,
|
|
75
|
+
dds_cli.exceptions.ApiResponseError,
|
|
76
|
+
dds_cli.exceptions.DDSCLIException,
|
|
71
77
|
) as error:
|
|
72
78
|
self.notify(f"Error: {error}", severity="error")
|
|
73
|
-
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
74
81
|
|
|
75
82
|
def confirm_2factor_code(self) -> None:
|
|
76
83
|
"""Confirm the 2FA code."""
|
|
77
84
|
code = self.query_one("#code").value
|
|
78
85
|
|
|
86
|
+
if not code:
|
|
87
|
+
self.notify("Error: 2FA code is required.", severity="error", timeout=10)
|
|
88
|
+
return
|
|
89
|
+
|
|
79
90
|
try:
|
|
80
91
|
self.auth.confirm_twofactor(
|
|
81
92
|
partial_auth_token=self.partial_auth_token,
|
|
@@ -84,7 +95,12 @@ class AuthenticationForm(Container):
|
|
|
84
95
|
)
|
|
85
96
|
self.notify("Successfully authenticated.")
|
|
86
97
|
self.app.set_auth_status(True)
|
|
87
|
-
except
|
|
98
|
+
except (
|
|
99
|
+
dds_cli.exceptions.AuthenticationError,
|
|
100
|
+
dds_cli.exceptions.ApiRequestError,
|
|
101
|
+
dds_cli.exceptions.ApiResponseError,
|
|
102
|
+
dds_cli.exceptions.DDSCLIException,
|
|
103
|
+
) as error:
|
|
88
104
|
self.notify(f"Error: {error}", severity="error", timeout=10)
|
|
89
105
|
self.close_modal()
|
|
90
106
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""DDS Project View Page"""
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
|
-
|
|
5
4
|
from textual.app import ComposeResult
|
|
6
5
|
from textual.containers import Vertical, Horizontal
|
|
7
6
|
from textual.widget import Widget
|
|
8
7
|
from textual.widgets import Placeholder
|
|
8
|
+
|
|
9
9
|
from dds_cli.dds_gui.pages.authentication.authentication import Authentication
|
|
10
|
+
from dds_cli.dds_gui.pages.project_content.project_content import ProjectContent
|
|
11
|
+
from dds_cli.dds_gui.pages.project_list.project_list import ProjectList
|
|
10
12
|
from dds_cli.dds_gui.pages.important_information.important_information import ImportantInformation
|
|
13
|
+
from dds_cli.dds_gui.pages.project_information.project_information import ProjectInformation
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class ProjectView(Widget):
|
|
@@ -51,21 +54,15 @@ class ProjectView(Widget):
|
|
|
51
54
|
def compose(self) -> ComposeResult:
|
|
52
55
|
with Horizontal():
|
|
53
56
|
with Vertical(id="left-container"):
|
|
54
|
-
yield
|
|
55
|
-
id="project-list"
|
|
56
|
-
) # ProjectList(title="Projects", id="project-list")
|
|
57
|
+
yield ProjectList(title="Projects", id="project-list")
|
|
57
58
|
yield ImportantInformation(
|
|
58
59
|
title="Important Information", id="important-information"
|
|
59
60
|
) # ImportantInformation(title="Important Information", id="important-information")
|
|
60
61
|
yield Authentication(title="Authentication", id="auth-menu")
|
|
61
62
|
with Vertical(id="right-container"):
|
|
62
63
|
with Horizontal(id="top-container"):
|
|
63
|
-
yield
|
|
64
|
-
|
|
65
|
-
) # ProjectContent(title="Project Content", id="project-content")
|
|
66
|
-
yield Placeholder(
|
|
67
|
-
id="project-information"
|
|
68
|
-
) # ProjectInformation(title="Project Information", id="project-information")
|
|
64
|
+
yield ProjectContent(title="Project Content", id="project-content")
|
|
65
|
+
yield ProjectInformation(title="Project Information", id="project-information")
|
|
69
66
|
with Horizontal(id="bottom-container"):
|
|
70
67
|
yield Placeholder(
|
|
71
68
|
id="project-actions"
|
|
@@ -234,7 +234,7 @@ class ProjectStatusManager(base.DDSBaseClass):
|
|
|
234
234
|
f"\nThe new deadline for project {project_id} will be: [b][blue]{new_deadline_date}[/b][/blue]"
|
|
235
235
|
"\n\n[b][blue]Are you sure [/b][/blue]you want to perform this operation? "
|
|
236
236
|
"\nYou can only extend the data availability a maximum of "
|
|
237
|
-
"[b][blue]
|
|
237
|
+
"[b][blue]2 times[/b][/blue], this consumes one of those times."
|
|
238
238
|
)
|
|
239
239
|
|
|
240
240
|
if not rich.prompt.Confirm.ask(prompt_question):
|
|
@@ -15,6 +15,7 @@ import botocore
|
|
|
15
15
|
|
|
16
16
|
# Own modules
|
|
17
17
|
import dds_cli.utils
|
|
18
|
+
from dds_cli import constants
|
|
18
19
|
from dds_cli import DDSEndpoint
|
|
19
20
|
|
|
20
21
|
###############################################################################
|
|
@@ -75,6 +76,14 @@ class S3Connector:
|
|
|
75
76
|
endpoint_url=self.url,
|
|
76
77
|
aws_access_key_id=self.keys["access_key"],
|
|
77
78
|
aws_secret_access_key=self.keys["secret_key"],
|
|
79
|
+
config=botocore.client.Config(
|
|
80
|
+
read_timeout=constants.READ_TIMEOUT,
|
|
81
|
+
connect_timeout=constants.CONNECT_TIMEOUT,
|
|
82
|
+
retries={
|
|
83
|
+
"max_attempts": 10,
|
|
84
|
+
# TODO: Add retry strategy mode="standard" when boto3 version >= 1.26.0
|
|
85
|
+
},
|
|
86
|
+
),
|
|
78
87
|
)
|
|
79
88
|
except (boto3.exceptions.Boto3Error, botocore.exceptions.BotoCoreError) as err:
|
|
80
89
|
LOG.warning("S3 connection failed: %s", err)
|
|
@@ -76,7 +76,7 @@ class User:
|
|
|
76
76
|
LOG.debug("Starting authentication on the API...")
|
|
77
77
|
|
|
78
78
|
# If no username or password is provided, prompt for them
|
|
79
|
-
if not username
|
|
79
|
+
if not username or not password:
|
|
80
80
|
if self.no_prompt:
|
|
81
81
|
raise exceptions.AuthenticationError(
|
|
82
82
|
message=(
|
|
@@ -88,12 +88,12 @@ class User:
|
|
|
88
88
|
username = Prompt.ask("DDS username")
|
|
89
89
|
password = getpass.getpass(prompt="DDS password: ")
|
|
90
90
|
|
|
91
|
-
if username
|
|
91
|
+
if not username:
|
|
92
92
|
raise exceptions.AuthenticationError(
|
|
93
93
|
message="Non-empty username needed to be able to authenticate."
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
if password
|
|
96
|
+
if not password:
|
|
97
97
|
raise exceptions.AuthenticationError(
|
|
98
98
|
message="Non-empty password needed to be able to authenticate."
|
|
99
99
|
)
|
|
@@ -289,7 +289,7 @@ class TokenFile:
|
|
|
289
289
|
if token_path is None:
|
|
290
290
|
self.token_file = dds_cli.TOKEN_FILE
|
|
291
291
|
else:
|
|
292
|
-
self.token_file = pathlib.Path(
|
|
292
|
+
self.token_file = pathlib.Path(token_path).expanduser()
|
|
293
293
|
|
|
294
294
|
def read_token(self):
|
|
295
295
|
"""Attempts to fetch a valid token from the token file.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dds_cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.13.0
|
|
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
|
|
@@ -18,7 +18,7 @@ Requires-Dist: boto3==1.24.73
|
|
|
18
18
|
Requires-Dist: botocore==1.27.73
|
|
19
19
|
Requires-Dist: click==8.1.3
|
|
20
20
|
Requires-Dist: click-pathlib==2020.3.13.0
|
|
21
|
-
Requires-Dist: cryptography==
|
|
21
|
+
Requires-Dist: cryptography==44.0.1
|
|
22
22
|
Requires-Dist: immutabledict==2.2.1
|
|
23
23
|
Requires-Dist: jwcrypto==1.5.6
|
|
24
24
|
Requires-Dist: prettytable==3.7.0
|
|
@@ -27,7 +27,7 @@ Requires-Dist: PyNaCl==1.5.0
|
|
|
27
27
|
Requires-Dist: pytz==2022.2.1
|
|
28
28
|
Requires-Dist: PyYAML==6.0.2
|
|
29
29
|
Requires-Dist: questionary==1.10.0
|
|
30
|
-
Requires-Dist: requests==2.32.
|
|
30
|
+
Requires-Dist: requests==2.32.4
|
|
31
31
|
Requires-Dist: rich==13.6.0
|
|
32
32
|
Requires-Dist: rich-click==1.5.2
|
|
33
33
|
Requires-Dist: simplejson==3.17.6
|
|
@@ -7,6 +7,7 @@ dds_cli/__main__.py
|
|
|
7
7
|
dds_cli/account_manager.py
|
|
8
8
|
dds_cli/auth.py
|
|
9
9
|
dds_cli/base.py
|
|
10
|
+
dds_cli/constants.py
|
|
10
11
|
dds_cli/custom_decorators.py
|
|
11
12
|
dds_cli/data_getter.py
|
|
12
13
|
dds_cli/data_lister.py
|
|
@@ -54,7 +55,6 @@ dds_cli/dds_gui/components/dds_modal.py
|
|
|
54
55
|
dds_cli/dds_gui/components/dds_select.py
|
|
55
56
|
dds_cli/dds_gui/components/dds_status_chip.py
|
|
56
57
|
dds_cli/dds_gui/components/dds_text_item.py
|
|
57
|
-
dds_cli/dds_gui/components/dds_tree_view.py
|
|
58
58
|
dds_cli/dds_gui/pages/__init__.py
|
|
59
59
|
dds_cli/dds_gui/pages/project_view.py
|
|
60
60
|
dds_cli/dds_gui/pages/authentication/__init__.py
|
|
@@ -66,9 +66,6 @@ dds_cli/dds_gui/pages/authentication/modals/logout_modal.py
|
|
|
66
66
|
dds_cli/dds_gui/pages/authentication/modals/reauthenticate_modal.py
|
|
67
67
|
dds_cli/dds_gui/pages/project_view_mode/__init__.py
|
|
68
68
|
dds_cli/dds_gui/pages/project_view_mode/project_actions.py
|
|
69
|
-
dds_cli/dds_gui/pages/project_view_mode/project_content.py
|
|
70
|
-
dds_cli/dds_gui/pages/project_view_mode/project_information.py
|
|
71
|
-
dds_cli/dds_gui/pages/project_view_mode/project_list.py
|
|
72
69
|
dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/__init__.py
|
|
73
70
|
dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/download_data.py
|
|
74
71
|
dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/user_access.py
|
|
@@ -81,15 +78,20 @@ tests/__init__.py
|
|
|
81
78
|
tests/test_account_manager.py
|
|
82
79
|
tests/test_auth.py
|
|
83
80
|
tests/test_base.py
|
|
81
|
+
tests/test_data_getter.py
|
|
84
82
|
tests/test_data_remover.py
|
|
85
83
|
tests/test_file_compressor.py
|
|
86
84
|
tests/test_file_encryptor.py
|
|
87
85
|
tests/test_file_handler_local.py
|
|
88
86
|
tests/test_motd_manager.py
|
|
89
87
|
tests/test_project_status.py
|
|
88
|
+
tests/test_s3_connector.py
|
|
90
89
|
tests/test_superadmin_helper.py
|
|
91
90
|
tests/test_user.py
|
|
92
91
|
tests/test_utils.py
|
|
93
92
|
tests/gui_tests/__init__.py
|
|
94
93
|
tests/gui_tests/test_authentication.py
|
|
95
|
-
tests/gui_tests/test_important_information.py
|
|
94
|
+
tests/gui_tests/test_important_information.py
|
|
95
|
+
tests/gui_tests/test_project_content.py
|
|
96
|
+
tests/gui_tests/test_project_information.py
|
|
97
|
+
tests/gui_tests/test_project_list.py
|
|
@@ -2,7 +2,7 @@ boto3==1.24.73
|
|
|
2
2
|
botocore==1.27.73
|
|
3
3
|
click==8.1.3
|
|
4
4
|
click-pathlib==2020.3.13.0
|
|
5
|
-
cryptography==
|
|
5
|
+
cryptography==44.0.1
|
|
6
6
|
immutabledict==2.2.1
|
|
7
7
|
jwcrypto==1.5.6
|
|
8
8
|
prettytable==3.7.0
|
|
@@ -11,7 +11,7 @@ PyNaCl==1.5.0
|
|
|
11
11
|
pytz==2022.2.1
|
|
12
12
|
PyYAML==6.0.2
|
|
13
13
|
questionary==1.10.0
|
|
14
|
-
requests==2.32.
|
|
14
|
+
requests==2.32.4
|
|
15
15
|
rich==13.6.0
|
|
16
16
|
rich-click==1.5.2
|
|
17
17
|
simplejson==3.17.6
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
This file is used to run the DDS GUI standalone for the executable.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
5
7
|
from dds_cli.dds_gui.app import DDSApp
|
|
6
8
|
|
|
7
9
|
if __name__ == "__main__":
|
|
8
|
-
app = DDSApp(token_path="
|
|
10
|
+
app = DDSApp(token_path=pathlib.Path.home() / ".dds_cli_token")
|
|
9
11
|
app.run()
|