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.
Files changed (106) hide show
  1. {dds_cli-2.12.0 → dds_cli-2.13.0}/PKG-INFO +3 -3
  2. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/__init__.py +1 -1
  3. dds_cli-2.13.0/dds_cli/constants.py +14 -0
  4. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/data_getter.py +6 -2
  5. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/app.py +2 -2
  6. dds_cli-2.13.0/dds_cli/dds_gui/dds_state_manager.py +165 -0
  7. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/authentication_form.py +21 -5
  8. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view.py +7 -10
  9. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/project_status.py +1 -1
  10. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/s3_connector.py +9 -0
  11. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/user.py +4 -4
  12. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/version.py +1 -1
  13. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/PKG-INFO +3 -3
  14. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/SOURCES.txt +7 -5
  15. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/requires.txt +2 -2
  16. {dds_cli-2.12.0 → dds_cli-2.13.0}/gui_build/gui_standalone.py +3 -1
  17. dds_cli-2.13.0/tests/gui_tests/test_authentication.py +1567 -0
  18. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/gui_tests/test_important_information.py +13 -10
  19. dds_cli-2.13.0/tests/gui_tests/test_project_content.py +1000 -0
  20. dds_cli-2.13.0/tests/gui_tests/test_project_information.py +650 -0
  21. dds_cli-2.13.0/tests/gui_tests/test_project_list.py +558 -0
  22. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_auth.py +5 -3
  23. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_base.py +9 -7
  24. dds_cli-2.13.0/tests/test_data_getter.py +133 -0
  25. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_data_remover.py +17 -20
  26. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_file_compressor.py +17 -25
  27. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_file_encryptor.py +1 -2
  28. dds_cli-2.13.0/tests/test_file_handler_local.py +104 -0
  29. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_motd_manager.py +15 -13
  30. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_project_status.py +19 -28
  31. dds_cli-2.13.0/tests/test_s3_connector.py +112 -0
  32. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_superadmin_helper.py +3 -3
  33. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_user.py +97 -20
  34. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/test_utils.py +26 -16
  35. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_tree_view.py +0 -55
  36. dds_cli-2.12.0/dds_cli/dds_gui/dds_state_manager.py +0 -202
  37. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_content.py +0 -31
  38. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_information.py +0 -99
  39. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_list.py +0 -37
  40. dds_cli-2.12.0/tests/gui_tests/test_authentication.py +0 -244
  41. dds_cli-2.12.0/tests/test_file_handler_local.py +0 -90
  42. {dds_cli-2.12.0 → dds_cli-2.13.0}/LICENSE +0 -0
  43. {dds_cli-2.12.0 → dds_cli-2.13.0}/README.md +0 -0
  44. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/__main__.py +0 -0
  45. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/account_manager.py +0 -0
  46. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/auth.py +0 -0
  47. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/base.py +0 -0
  48. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/custom_decorators.py +0 -0
  49. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/data_lister.py +0 -0
  50. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/data_putter.py +0 -0
  51. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/data_remover.py +0 -0
  52. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/__init__.py +0 -0
  53. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/__init__.py +0 -0
  54. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_button.py +0 -0
  55. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_container.py +0 -0
  56. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_footer.py +0 -0
  57. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_form.py +0 -0
  58. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_input.py +0 -0
  59. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_modal.py +0 -0
  60. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_select.py +0 -0
  61. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_status_chip.py +0 -0
  62. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/components/dds_text_item.py +0 -0
  63. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/__init__.py +0 -0
  64. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/__init__.py +0 -0
  65. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/authentication.py +0 -0
  66. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/modals/__init__.py +0 -0
  67. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/modals/login_modal.py +0 -0
  68. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/modals/logout_modal.py +0 -0
  69. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/authentication/modals/reauthenticate_modal.py +0 -0
  70. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view_mode/__init__.py +0 -0
  71. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/pages/project_view_mode/project_actions.py +0 -0
  72. {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
  73. {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
  74. {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
  75. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/types/__init__.py +0 -0
  76. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/types/dds_severity_types.py +0 -0
  77. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/dds_gui/types/dds_status_types.py +0 -0
  78. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/directory.py +0 -0
  79. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/exceptions.py +0 -0
  80. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_compressor.py +0 -0
  81. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_encryptor.py +0 -0
  82. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_handler.py +0 -0
  83. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_handler_local.py +0 -0
  84. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/file_handler_remote.py +0 -0
  85. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/message_helper.py +0 -0
  86. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/motd_manager.py +0 -0
  87. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/options.py +0 -0
  88. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/project_creator.py +0 -0
  89. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/project_info.py +0 -0
  90. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/status.py +0 -0
  91. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/superadmin_helper.py +0 -0
  92. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/text_handler.py +0 -0
  93. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/timestamp.py +0 -0
  94. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/unit_manager.py +0 -0
  95. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli/utils.py +0 -0
  96. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/dependency_links.txt +0 -0
  97. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/entry_points.txt +0 -0
  98. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/not-zip-safe +0 -0
  99. {dds_cli-2.12.0 → dds_cli-2.13.0}/dds_cli.egg-info/top_level.txt +0 -0
  100. {dds_cli-2.12.0 → dds_cli-2.13.0}/gui_build/__init__.py +0 -0
  101. {dds_cli-2.12.0 → dds_cli-2.13.0}/pyproject.toml +0 -0
  102. {dds_cli-2.12.0 → dds_cli-2.13.0}/setup.cfg +0 -0
  103. {dds_cli-2.12.0 → dds_cli-2.13.0}/setup.py +0 -0
  104. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/__init__.py +0 -0
  105. {dds_cli-2.12.0 → dds_cli-2.13.0}/tests/gui_tests/__init__.py +0 -0
  106. {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.12.0
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==42.0.4
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.2
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(os.path.expanduser("~/.dds_cli_token"))
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
- # TODO: Set timeout? (pylint)
239
- with requests.get(file_remote, stream=True) as req:
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 App, ComposeResult
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(App, DDSStateManager):
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)
@@ -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 self.auth:
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) -> None:
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
- self.auth = None
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 dds_cli.exceptions.AuthenticationError as error:
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 Placeholder(
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 Placeholder(
64
- id="project-content"
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]3 times[/b][/blue], this consumes one of those times."
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 and not password:
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(os.path.expanduser(token_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.
@@ -2,4 +2,4 @@
2
2
 
3
3
  # Do not change bump the major version unless absolutely necessary - makes incompatible with API
4
4
  # If mid or minor version reaches 9, continue to 10, 11 etc.
5
- __version__ = "2.12.0"
5
+ __version__ = "2.13.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dds_cli
3
- Version: 2.12.0
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==42.0.4
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.2
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==42.0.4
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.2
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="~/.dds_cli_token")
10
+ app = DDSApp(token_path=pathlib.Path.home() / ".dds_cli_token")
9
11
  app.run()