dds-cli 2.11.0__tar.gz → 2.12.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 (98) hide show
  1. {dds_cli-2.11.0 → dds_cli-2.12.0}/PKG-INFO +3 -2
  2. {dds_cli-2.11.0 → dds_cli-2.12.0}/README.md +1 -1
  3. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/__main__.py +52 -12
  4. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/auth.py +75 -13
  5. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/base.py +11 -2
  6. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/custom_decorators.py +1 -5
  7. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/data_getter.py +58 -6
  8. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/data_putter.py +17 -8
  9. dds_cli-2.12.0/dds_cli/dds_gui/__init__.py +6 -0
  10. dds_cli-2.12.0/dds_cli/dds_gui/app.py +71 -0
  11. dds_cli-2.12.0/dds_cli/dds_gui/components/__init__.py +1 -0
  12. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_button.py +65 -0
  13. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_container.py +80 -0
  14. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_footer.py +17 -0
  15. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_form.py +27 -0
  16. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_input.py +19 -0
  17. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_modal.py +138 -0
  18. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_select.py +23 -0
  19. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_status_chip.py +61 -0
  20. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_text_item.py +17 -0
  21. dds_cli-2.12.0/dds_cli/dds_gui/components/dds_tree_view.py +55 -0
  22. dds_cli-2.12.0/dds_cli/dds_gui/dds_state_manager.py +202 -0
  23. dds_cli-2.12.0/dds_cli/dds_gui/pages/__init__.py +1 -0
  24. dds_cli-2.12.0/dds_cli/dds_gui/pages/authentication/__init__.py +1 -0
  25. dds_cli-2.12.0/dds_cli/dds_gui/pages/authentication/authentication.py +56 -0
  26. dds_cli-2.12.0/dds_cli/dds_gui/pages/authentication/authentication_form.py +122 -0
  27. dds_cli-2.12.0/dds_cli/dds_gui/pages/authentication/modals/__init__.py +1 -0
  28. dds_cli-2.12.0/dds_cli/dds_gui/pages/authentication/modals/login_modal.py +35 -0
  29. dds_cli-2.12.0/dds_cli/dds_gui/pages/authentication/modals/logout_modal.py +28 -0
  30. dds_cli-2.12.0/dds_cli/dds_gui/pages/authentication/modals/reauthenticate_modal.py +35 -0
  31. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view.py +72 -0
  32. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/__init__.py +1 -0
  33. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_actions.py +32 -0
  34. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/__init__.py +1 -0
  35. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/download_data.py +61 -0
  36. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_actions_tabs/user_access.py +41 -0
  37. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_content.py +31 -0
  38. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_information.py +99 -0
  39. dds_cli-2.12.0/dds_cli/dds_gui/pages/project_view_mode/project_list.py +37 -0
  40. dds_cli-2.12.0/dds_cli/dds_gui/types/__init__.py +1 -0
  41. dds_cli-2.12.0/dds_cli/dds_gui/types/dds_severity_types.py +12 -0
  42. dds_cli-2.12.0/dds_cli/dds_gui/types/dds_status_types.py +14 -0
  43. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/exceptions.py +8 -0
  44. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/file_compressor.py +7 -6
  45. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/file_encryptor.py +22 -10
  46. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/file_handler_local.py +8 -3
  47. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/file_handler_remote.py +1 -1
  48. dds_cli-2.12.0/dds_cli/message_helper.py +72 -0
  49. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/motd_manager.py +41 -38
  50. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/s3_connector.py +1 -1
  51. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/user.py +130 -91
  52. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/version.py +1 -1
  53. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli.egg-info/PKG-INFO +3 -2
  54. dds_cli-2.12.0/dds_cli.egg-info/SOURCES.txt +95 -0
  55. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli.egg-info/requires.txt +1 -0
  56. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli.egg-info/top_level.txt +1 -0
  57. dds_cli-2.12.0/gui_build/gui_standalone.py +9 -0
  58. dds_cli-2.12.0/tests/__init__.py +0 -0
  59. dds_cli-2.12.0/tests/gui_tests/__init__.py +0 -0
  60. dds_cli-2.12.0/tests/gui_tests/test_authentication.py +244 -0
  61. dds_cli-2.12.0/tests/gui_tests/test_important_information.py +455 -0
  62. dds_cli-2.12.0/tests/test_auth.py +628 -0
  63. dds_cli-2.12.0/tests/test_base.py +228 -0
  64. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_file_compressor.py +15 -11
  65. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_file_encryptor.py +6 -3
  66. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_motd_manager.py +75 -24
  67. dds_cli-2.12.0/tests/test_user.py +467 -0
  68. dds_cli-2.11.0/dds_cli.egg-info/SOURCES.txt +0 -52
  69. {dds_cli-2.11.0 → dds_cli-2.12.0}/LICENSE +0 -0
  70. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/__init__.py +0 -0
  71. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/account_manager.py +0 -0
  72. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/data_lister.py +0 -0
  73. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/data_remover.py +0 -0
  74. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/directory.py +0 -0
  75. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/file_handler.py +0 -0
  76. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/options.py +0 -0
  77. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/project_creator.py +0 -0
  78. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/project_info.py +0 -0
  79. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/project_status.py +0 -0
  80. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/status.py +0 -0
  81. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/superadmin_helper.py +0 -0
  82. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/text_handler.py +0 -0
  83. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/timestamp.py +0 -0
  84. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/unit_manager.py +0 -0
  85. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli/utils.py +0 -0
  86. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli.egg-info/dependency_links.txt +0 -0
  87. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli.egg-info/entry_points.txt +0 -0
  88. {dds_cli-2.11.0 → dds_cli-2.12.0}/dds_cli.egg-info/not-zip-safe +0 -0
  89. {dds_cli-2.11.0/tests → dds_cli-2.12.0/gui_build}/__init__.py +0 -0
  90. {dds_cli-2.11.0 → dds_cli-2.12.0}/pyproject.toml +0 -0
  91. {dds_cli-2.11.0 → dds_cli-2.12.0}/setup.cfg +0 -0
  92. {dds_cli-2.11.0 → dds_cli-2.12.0}/setup.py +0 -0
  93. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_account_manager.py +0 -0
  94. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_data_remover.py +0 -0
  95. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_file_handler_local.py +0 -0
  96. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_project_status.py +0 -0
  97. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_superadmin_helper.py +0 -0
  98. {dds_cli-2.11.0 → dds_cli-2.12.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dds_cli
3
- Version: 2.11.0
3
+ Version: 2.12.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
@@ -33,6 +33,7 @@ Requires-Dist: rich-click==1.5.2
33
33
  Requires-Dist: simplejson==3.17.6
34
34
  Requires-Dist: tzlocal==4.2
35
35
  Requires-Dist: zstandard==0.23.0
36
+ Requires-Dist: textual==2.1.2
36
37
  Dynamic: author
37
38
  Dynamic: classifier
38
39
  Dynamic: description
@@ -60,7 +61,7 @@ Dynamic: summary
60
61
  <a href="https://opensource.org/licenses/MIT">
61
62
  <img alt="Licence: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg">
62
63
  </a>
63
- <a href="[https://opensource.org/licenses/MIT](https://github.com/psf/black)">
64
+ <a href="https://github.com/psf/black">
64
65
  <img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg">
65
66
  </a>
66
67
  <a href="https://prettier.io/">
@@ -15,7 +15,7 @@
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://opensource.org/licenses/MIT](https://github.com/psf/black)">
18
+ <a href="https://github.com/psf/black">
19
19
  <img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg">
20
20
  </a>
21
21
  <a href="https://prettier.io/">
@@ -38,6 +38,7 @@ import dds_cli.project_status
38
38
  import dds_cli.project_info
39
39
  import dds_cli.user
40
40
  import dds_cli.utils
41
+ import dds_cli.message_helper
41
42
  from dds_cli.options import (
42
43
  destination_option,
43
44
  email_arg,
@@ -60,6 +61,8 @@ from dds_cli.options import (
60
61
  users_flag,
61
62
  )
62
63
 
64
+ # import dds_cli.dds_gui.app
65
+
63
66
  ####################################################################################################
64
67
  # START LOGGING CONFIG ###################################################### START LOGGING CONFIG #
65
68
  ####################################################################################################
@@ -95,11 +98,24 @@ dds_cli.utils.stderr_console.print(
95
98
  )
96
99
 
97
100
  if len(sys.argv) == 1 or (len(sys.argv) > 1 and sys.argv[1] != "motd"):
98
- motds = dds_cli.motd_manager.MotdManager.list_all_active_motds(table=False)
99
- if motds:
100
- dds_cli.utils.stderr_console.print("[bold]Important information:[/bold]")
101
- for motd in motds:
102
- dds_cli.utils.stderr_console.print(f"{motd['Created']} - {motd['Message']} \n")
101
+ try:
102
+ motds = dds_cli.motd_manager.MotdManager.list_all_active_motds(table=False)
103
+ if motds:
104
+ dds_cli.utils.stderr_console.print("[bold]Important information:[/bold]")
105
+ for motd in motds:
106
+ dds_cli.utils.stderr_console.print(f"{motd['Created']} - {motd['Message']} \n")
107
+ except dds_cli.exceptions.NoMOTDsError as no_motds_err:
108
+ # Print message about no MOTD
109
+ LOG.info(no_motds_err)
110
+ except (
111
+ dds_cli.exceptions.ApiResponseError,
112
+ dds_cli.exceptions.ApiRequestError,
113
+ ) as api_err:
114
+ # Avoid breaking CLI startup on MOTD fetch issues
115
+ LOG.debug("Skipping MOTD display due to API error: %s", api_err)
116
+ except dds_cli.exceptions.DDSCLIException as dds_cli_err:
117
+ # Covers 400/403 and other handled DDS CLI errors from perform_request
118
+ LOG.debug("Skipping MOTD display due to DDS error: %s", dds_cli_err)
103
119
 
104
120
 
105
121
  # -- dds -- #
@@ -193,6 +209,21 @@ def dds_main(click_ctx, verbose, force_no_log, log_file, no_prompt, token_path):
193
209
  click_ctx.obj.update({"DEFAULT_LOG": False})
194
210
 
195
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()
225
+
226
+
196
227
  # ************************************************************************************************ #
197
228
  # MAIN DDS COMMANDS ************************************************************ MAIN DDS COMMANDS #
198
229
  # ************************************************************************************************ #
@@ -425,7 +456,9 @@ def login(click_ctx, totp, allow_group):
425
456
  LOG.warning("The --no-prompt flag is ignored for `dds auth login`")
426
457
  try:
427
458
  with dds_cli.auth.Auth(
428
- token_path=click_ctx.get("TOKEN_PATH"), totp=totp, allow_group=allow_group
459
+ token_path=click_ctx.get("TOKEN_PATH"),
460
+ totp=totp,
461
+ allow_group=allow_group,
429
462
  ):
430
463
  # Authentication token renewed in the init method.
431
464
  LOG.info("[green] :white_check_mark: Authentication successful![/green]")
@@ -452,7 +485,8 @@ def logout(click_ctx):
452
485
  with dds_cli.auth.Auth(
453
486
  authenticate=False, token_path=click_ctx.get("TOKEN_PATH")
454
487
  ) as authenticator:
455
- authenticator.logout()
488
+ logout_ok = authenticator.logout()
489
+ dds_cli.message_helper.CLIMessageHelper().logout_message(logout_ok=logout_ok)
456
490
 
457
491
  except (dds_cli.exceptions.DDSCLIException, dds_cli.exceptions.ApiRequestError) as err:
458
492
  LOG.error(err)
@@ -474,7 +508,13 @@ def info(click_ctx):
474
508
  with dds_cli.auth.Auth(
475
509
  authenticate=False, token_path=click_ctx.get("TOKEN_PATH")
476
510
  ) as authenticator:
477
- authenticator.check()
511
+ expiration_time = authenticator.check()
512
+ if expiration_time:
513
+ dds_cli.message_helper.CLIMessageHelper().token_report_message(
514
+ expiration_time=expiration_time
515
+ )
516
+ else:
517
+ dds_cli.message_helper.CLIMessageHelper().token_expired_message()
478
518
  except (dds_cli.exceptions.DDSCLIException, dds_cli.exceptions.ApiRequestError) as err:
479
519
  LOG.error(err)
480
520
  sys.exit(1)
@@ -744,12 +784,12 @@ def delete_user(click_ctx, email, self, is_invite):
744
784
  proceed_deletion = True
745
785
  else:
746
786
  if is_invite and self:
747
- LOG.error("You cannot specify both `--self` and `--is-invite. Choose one.")
787
+ LOG.error("You cannot specify both `--self` and `--is-invite`. Choose one.")
748
788
  sys.exit(0)
749
789
 
750
790
  if not self and not email:
751
791
  LOG.error(
752
- "You must specify an email adress associated to the user you're requesting to delete."
792
+ "You must specify an email address associated to the user you're requesting to delete."
753
793
  )
754
794
  sys.exit(0)
755
795
 
@@ -1837,7 +1877,6 @@ def get_data(
1837
1877
 
1838
1878
  # Schedule the first num_threads futures for upload
1839
1879
  for file in itertools.islice(iterator, num_threads):
1840
- LOG.debug("Starting: %s", rich.markup.escape(str(file)))
1841
1880
  # Execute download
1842
1881
  download_threads[
1843
1882
  texec.submit(getter.download_and_verify, file=file, progress=progress)
@@ -1876,7 +1915,6 @@ def get_data(
1876
1915
 
1877
1916
  # Schedule the next set of futures for download
1878
1917
  for next_file in itertools.islice(iterator, new_tasks):
1879
- LOG.debug("Starting: %s", rich.markup.escape(str(next_file)))
1880
1918
  # Execute download
1881
1919
  download_threads[
1882
1920
  texec.submit(
@@ -2120,6 +2158,8 @@ def list_active_motds(click_ctx):
2120
2158
  ) as err:
2121
2159
  LOG.error(err)
2122
2160
  sys.exit(1)
2161
+ except dds_cli.exceptions.NoMOTDsError as err:
2162
+ LOG.info(err)
2123
2163
 
2124
2164
 
2125
2165
  # -- dds motd deactivate -- #
@@ -3,6 +3,8 @@
3
3
  # Standard library
4
4
  import logging
5
5
  import getpass
6
+ from datetime import datetime
7
+ from typing import Optional
6
8
 
7
9
  # Installed
8
10
  from rich.prompt import Prompt
@@ -39,6 +41,10 @@ class Auth(base.DDSBaseClass):
39
41
  ):
40
42
  """Handle actions regarding session management in DDS."""
41
43
  # Initiate DDSBaseClass to authenticate user
44
+ # Will authenticate user automatically if authenticate is True,
45
+ # else need to call login and confirm_twofactor methods to authenticate user
46
+ # This is to be able to use the auth class in the GUI code,
47
+ # where the user is not prompted for username and password
42
48
  super().__init__(
43
49
  authenticate=authenticate,
44
50
  force_renew_token=force_renew_token,
@@ -47,27 +53,83 @@ class Auth(base.DDSBaseClass):
47
53
  allow_group=allow_group,
48
54
  )
49
55
 
50
- def check(self):
51
- """Check if token exists and return info."""
56
+ self.allow_group = allow_group
57
+
58
+ def login(self, username: Optional[str] = None, password: Optional[str] = None) -> tuple:
59
+ """Login user to DDS. Used to manually authenticate users with username and password.
60
+ If not provided, will prompt for them. Currently only used in the GUI.
61
+
62
+ :param username: The username to login with.
63
+ :param password: The password to login with.
64
+
65
+ :return: Partial auth token and second factor method
66
+ """
67
+ # Create a User instance to call the login method
68
+ user_instance = user.User(
69
+ force_renew_token=False,
70
+ no_prompt=False,
71
+ token_path=self.token_path,
72
+ allow_group=self.allow_group,
73
+ retrieve_token=False,
74
+ )
75
+ return user_instance.login(username, password)
76
+
77
+ def confirm_twofactor(
78
+ self,
79
+ partial_auth_token: str,
80
+ secondfactor_method: str,
81
+ totp: str = None,
82
+ twofactor_code: Optional[str] = None,
83
+ ):
84
+ """Confirm 2FA for user. Used to manually confirm the 2FA code.
85
+ If not provided, will prompt for it. Currently only used in the GUI.
86
+
87
+ Sets the token for the base class after confirming 2FA.
88
+
89
+ :param partial_auth_token: The partial auth token.
90
+ :param twofactor_code: The 2FA code to confirm.
91
+
92
+ """
93
+
94
+ user_instance = user.User(
95
+ force_renew_token=False,
96
+ token_path=self.token_path,
97
+ allow_group=self.allow_group,
98
+ totp=totp,
99
+ retrieve_token=False,
100
+ )
101
+
102
+ user_instance.confirm_twofactor(
103
+ partial_auth_token=partial_auth_token,
104
+ secondfactor_method=secondfactor_method,
105
+ totp=totp,
106
+ twofactor_code=twofactor_code,
107
+ )
108
+
109
+ self.set_token(user_instance.token_dict)
110
+
111
+ def check(self) -> Optional[datetime]:
112
+ """Check if token exists and returns the token expiration time.
113
+
114
+ :return: Token info if token exists, None otherwise.
115
+ """
52
116
  token_file = user.TokenFile(token_path=self.token_path)
53
117
  if token_file.file_exists():
54
118
  token = token_file.read_token()
55
119
  if token:
56
- token_file.token_report(token=token)
57
- else:
58
- LOG.info(
59
- "[red]No saved token found, or token has expired. "
60
- "Authenticate yourself with `dds auth login` to use this functionality![/red]"
61
- )
120
+ return token_file.token_report(token=token)
121
+ return None
122
+
123
+ def logout(self) -> bool:
124
+ """Logout user by removing authenticated token.
62
125
 
63
- def logout(self):
64
- """Logout user by removing authenticated token."""
126
+ :return: True if logout was successful, False if already logged out.
127
+ """
65
128
  token_file = user.TokenFile(token_path=self.token_path)
66
129
  if token_file.file_exists():
67
130
  token_file.delete_token()
68
- LOG.info("[green] :white_check_mark: Successfully logged out![/green]")
69
- else:
70
- LOG.info("[green]Already logged out![/green]")
131
+ return True
132
+ return False
71
133
 
72
134
  def twofactor(self, auth_method: str = None):
73
135
  """Perform 2FA for user."""
@@ -1,4 +1,4 @@
1
- """Base class for the DDS CLI. Verifies the users access to the DDS."""
1
+ """Base class for the DDS CLI. Verifies the user's access to the DDS."""
2
2
 
3
3
  ###############################################################################
4
4
  # IMPORTS ########################################################### IMPORTS #
@@ -58,6 +58,8 @@ class DDSBaseClass:
58
58
  self.no_prompt = no_prompt
59
59
  self.token_path = token_path
60
60
 
61
+ self.totp = totp
62
+
61
63
  # Keyboardinterrupt
62
64
  self.stop_doing = False
63
65
 
@@ -67,8 +69,8 @@ class DDSBaseClass:
67
69
  force_renew_token=force_renew_token,
68
70
  no_prompt=no_prompt,
69
71
  token_path=token_path,
70
- totp=totp,
71
72
  allow_group=allow_group,
73
+ totp=totp,
72
74
  )
73
75
  self.token = dds_user.token_dict
74
76
 
@@ -113,6 +115,13 @@ class DDSBaseClass:
113
115
 
114
116
  # Public methods ############################### Public methods #
115
117
 
118
+ def set_token(self, token_dict: dict) -> None:
119
+ """Sets the token for the base class.
120
+ Called from auth class to set the token when authenticating
121
+ in the gui without running the base class with authenticate set to true.
122
+ """
123
+ self.token = token_dict
124
+
116
125
  def get_project_info(self):
117
126
  """Collect project information from API."""
118
127
 
@@ -40,7 +40,7 @@ def verify_proceed(func):
40
40
  # Check if keyboardinterrupt in dds
41
41
  if self.stop_doing:
42
42
  # TODO (ina): Add save to status here
43
- message = "KeyBoardInterrupt - cancelling file {escape(file)}"
43
+ message = f"KeyboardInterrupt - cancelling file {escape(file)}"
44
44
  LOG.warning(message)
45
45
  return False # Do not proceed
46
46
 
@@ -52,7 +52,6 @@ def verify_proceed(func):
52
52
 
53
53
  # Mark as started
54
54
  self.status[file]["started"] = True
55
- LOG.debug("File '%s' started: %s", escape(str(file)), func.__name__)
56
55
 
57
56
  # Run function
58
57
  ok_to_proceed, message = func(self, file=file, *args, **kwargs)
@@ -101,7 +100,6 @@ def update_status(func):
101
100
 
102
101
  # Update status to started
103
102
  self.status[file][func.__name__].update({"started": True})
104
- LOG.debug("File '%s' status updated to %s: started", escape(str(file)), func.__name__)
105
103
 
106
104
  # Run function
107
105
  ok_to_continue, message, *_ = func(self, file=file, *args, **kwargs)
@@ -109,14 +107,12 @@ def update_status(func):
109
107
  # ok_to_continue = False
110
108
  if not ok_to_continue:
111
109
  # Save info about which operation failed
112
-
113
110
  self.status[file]["failed_op"] = func.__name__
114
111
  LOG.warning("%s failed: %s", func.__name__, message)
115
112
 
116
113
  else:
117
114
  # Update status to done
118
115
  self.status[file][func.__name__].update({"done": True})
119
- LOG.debug("File %s status updated to %s: done", escape(str(file)), func.__name__)
120
116
 
121
117
  return ok_to_continue, message
122
118
 
@@ -115,7 +115,9 @@ class DataGetter(base.DDSBaseClass):
115
115
  """Download the file, reveals the original data and verifies the integrity."""
116
116
  all_ok, message = (False, "")
117
117
  file_info = self.filehandler.data[file]
118
+ file_name_in_db = escape(str(file_info["name_in_db"]))
118
119
 
120
+ LOG.debug("Step 'download_and_verify': started file '%s'", file_name_in_db)
119
121
  # File task for downloading
120
122
  task = progress.add_task(
121
123
  description=txt.TextHandler.task_name(file=escape(str(file)), step="get"),
@@ -133,20 +135,49 @@ class DataGetter(base.DDSBaseClass):
133
135
  total=file_info["size_original"],
134
136
  )
135
137
 
136
- LOG.debug("File '%s' downloaded: %s", escape(str(file)), file_downloaded)
138
+ LOG.debug("File '%s' downloaded: %s", file_name_in_db, file_downloaded)
139
+
140
+ file_size_verified = False
137
141
 
138
142
  if file_downloaded:
143
+ ## File size verification
144
+ expected_size = file_info["size_stored"]
145
+ actual_size = file_info["path_downloaded"].stat().st_size
146
+
147
+ if actual_size == expected_size:
148
+ file_size_verified = True
149
+ LOG.debug(
150
+ "Downloaded file '%s' size matches expected size: %s bytes.",
151
+ file_name_in_db,
152
+ expected_size,
153
+ )
154
+ else:
155
+ LOG.debug(
156
+ "Downloaded file '%s' size mismatch: expected %s bytes, got %s bytes. Not decrypting.",
157
+ file_name_in_db,
158
+ expected_size,
159
+ actual_size,
160
+ )
161
+
162
+ if file_size_verified:
139
163
  db_updated, message = self.update_db(file=file)
140
- LOG.debug("Database updated: %s", db_updated)
164
+ LOG.debug(
165
+ "API call: database updated for file '%s': %s",
166
+ file_name_in_db,
167
+ db_updated,
168
+ )
141
169
 
142
- LOG.debug("Beginning decryption of file '%s'...", escape(str(file)))
170
+ LOG.debug("Beginning decryption of file '%s'...", file_name_in_db)
143
171
  file_saved = False
144
172
  with fe.Decryptor(
145
173
  project_keys=self.keys,
146
174
  peer_public=file_info["public_key"],
147
175
  key_salt=file_info["salt"],
176
+ files_directory=self.dds_directory.directories["FILES"],
148
177
  ) as decryptor:
149
- streamed_chunks = decryptor.decrypt_file(infile=file_info["path_downloaded"])
178
+ streamed_chunks = decryptor.decrypt_file(
179
+ infile=file_info["path_downloaded"], outfile=file
180
+ )
150
181
 
151
182
  stream_to_file_func = (
152
183
  fc.Compressor.decompress_filechunks
@@ -157,14 +188,35 @@ class DataGetter(base.DDSBaseClass):
157
188
  file_saved, message = stream_to_file_func(
158
189
  chunks=streamed_chunks,
159
190
  outfile=file,
191
+ files_directory=self.dds_directory.directories["FILES"],
160
192
  )
161
193
 
162
- LOG.debug("File saved? %s", file_saved)
194
+ LOG.debug("File '%s' saved? %s", file_name_in_db, file_saved)
163
195
  if file_saved:
196
+ # Check file size post-decryption and post-decompression
197
+ expected_size = file_info["size_original"]
198
+ actual_size = pathlib.Path(file).stat().st_size
199
+ if actual_size == expected_size:
200
+ LOG.debug(
201
+ "Decrypted file '%s' size matches expected size: %s bytes.",
202
+ file_name_in_db,
203
+ expected_size,
204
+ )
205
+ else:
206
+ LOG.debug(
207
+ "Decrypted file '%s' size mismatch: expected %s bytes, got %s bytes",
208
+ file_name_in_db,
209
+ expected_size,
210
+ actual_size,
211
+ )
164
212
  # TODO (ina): decide on checksum verification method --
165
213
  # this checks original, the other is generated from compressed
166
214
  all_ok, message = (
167
- fe.Encryptor.verify_checksum(file=file, correct_checksum=file_info["checksum"])
215
+ fe.Encryptor.verify_checksum(
216
+ file=file,
217
+ correct_checksum=file_info["checksum"],
218
+ files_directory=self.dds_directory.directories["FILES"],
219
+ )
168
220
  if self.verify_checksum
169
221
  else (True, "")
170
222
  )
@@ -95,7 +95,6 @@ def put(
95
95
 
96
96
  # Schedule the first num_threads futures for upload
97
97
  for file in itertools.islice(iterator, num_threads):
98
- LOG.debug("Starting: '%s'", escape(file))
99
98
  upload_threads[
100
99
  texec.submit(
101
100
  putter.protect_and_upload,
@@ -119,7 +118,7 @@ def put(
119
118
  # Get result from future and schedule database update
120
119
  for fut in done:
121
120
  uploaded_file = upload_threads.pop(fut)
122
- LOG.debug("Future done for file: %s", escape(uploaded_file))
121
+ LOG.debug("Future done for file: '%s'", escape(uploaded_file))
123
122
 
124
123
  # Get result
125
124
  try:
@@ -291,6 +290,8 @@ class DataPutter(base.DDSBaseClass):
291
290
  all_ok, saved, message = (False, False, "") # Error catching
292
291
  file_info = self.filehandler.data[file] # Info on current file
293
292
  file_public_key, salt = ("", "") # Crypto info
293
+ file_path_raw = escape(str(file_info["path_raw"]))
294
+ LOG.debug("Step '%s': started file '%s'", self.method, file_path_raw)
294
295
 
295
296
  # Progress bar for processing
296
297
  task = progress.add_task(
@@ -305,6 +306,7 @@ class DataPutter(base.DDSBaseClass):
305
306
  # Stream the chunks into the encryptor to save the encrypted chunks
306
307
  with fe.Encryptor(project_keys=self.keys) as encryptor:
307
308
  # Encrypt and save chunks
309
+ LOG.debug("Encrypting file '%s'", file_path_raw)
308
310
  saved, message = encryptor.encrypt_filechunks(
309
311
  chunks=streamed_chunks,
310
312
  outfile=file_info["path_processed"],
@@ -315,18 +317,21 @@ class DataPutter(base.DDSBaseClass):
315
317
  file_public_key = encryptor.get_public_component_hex(private_key=encryptor.my_private)
316
318
  salt = encryptor.salt
317
319
 
318
- LOG.debug("Updating file processed size: %s", file_info["path_processed"])
319
-
320
320
  # Update file info incl size, public key, salt
321
321
  self.filehandler.data[file]["public_key"] = file_public_key
322
322
  self.filehandler.data[file]["salt"] = salt
323
323
  self.filehandler.data[file]["size_processed"] = file_info["path_processed"].stat().st_size
324
324
 
325
+ LOG.debug(
326
+ "File '%s' processed size: %s",
327
+ file_path_raw,
328
+ file_info["path_processed"].stat().st_size,
329
+ )
330
+
325
331
  if saved:
326
332
  LOG.debug(
327
- "File successfully encrypted: '%s'. New location: '%s'",
328
- escape(file),
329
- escape(str(file_info["path_processed"])),
333
+ "File successfully encrypted: '%s'",
334
+ file_path_raw,
330
335
  )
331
336
  # Update progress bar for upload
332
337
  progress.reset(
@@ -346,7 +351,8 @@ class DataPutter(base.DDSBaseClass):
346
351
  if db_updated:
347
352
  all_ok = True
348
353
  LOG.debug(
349
- "File successfully uploaded and added to the database: '%s'", escape(file)
354
+ "File successfully uploaded and added to the database: '%s'",
355
+ file_path_raw,
350
356
  )
351
357
 
352
358
  if not saved or all_ok:
@@ -373,6 +379,8 @@ class DataPutter(base.DDSBaseClass):
373
379
  # File info
374
380
  file_local = str(self.filehandler.data[file]["path_processed"])
375
381
  file_remote = self.filehandler.data[file]["path_remote"]
382
+ file_path_raw = self.filehandler.data[file]["path_raw"]
383
+ LOG.debug("Step '%s': started file '%s'", self.method, file_path_raw)
376
384
 
377
385
  try:
378
386
  with self.s3connector as conn:
@@ -441,6 +449,7 @@ class DataPutter(base.DDSBaseClass):
441
449
  error_message=f"Failed to add file '{file}' to database",
442
450
  )
443
451
  added_to_db, message = (True, response_json)
452
+ LOG.debug("API call for file '%s: Adding to database'", fileinfo["path_raw"])
444
453
  except (
445
454
  dds_cli.exceptions.ApiRequestError,
446
455
  dds_cli.exceptions.ApiResponseError,
@@ -0,0 +1,6 @@
1
+ """DDS GUI package."""
2
+
3
+ # Make the GUI components available for import
4
+ from dds_cli.dds_gui.app import DDSApp
5
+
6
+ __all__ = ["DDSApp"]
@@ -0,0 +1,71 @@
1
+ """GUI Application for DDS CLI."""
2
+
3
+ from textual.app import App, ComposeResult
4
+ from textual.binding import Binding
5
+ from textual.widgets import Header
6
+ from textual.theme import Theme
7
+
8
+ from dds_cli.dds_gui.components.dds_footer import DDSFooter
9
+ from dds_cli.dds_gui.dds_state_manager import DDSStateManager
10
+ from dds_cli.dds_gui.pages.project_view import ProjectView
11
+
12
+ theme = Theme(
13
+ name="custom",
14
+ primary="#666666", # "#3F3F3F",
15
+ secondary="#4C979F",
16
+ # secondary="#12F0E1",
17
+ accent="#A7C947",
18
+ foreground="#FFFFFF",
19
+ panel="#045C64",
20
+ boost="#12F0E1",
21
+ warning="#F57C00",
22
+ error="#D32F2F",
23
+ success="#388E3C",
24
+ dark=True,
25
+ variables={
26
+ # "block-hover-background": "#43858B",
27
+ "block-hover-background": "#616060",
28
+ # "block-hover-foreground": "green",
29
+ # "primary-darken-2": "#323232",
30
+ # "block-cursor-blurred-background": "red",
31
+ # "block-cursor-background": "#12F0E1", #Tab color
32
+ },
33
+ )
34
+
35
+
36
+ class DDSApp(App, DDSStateManager):
37
+ """Textual App for DDS CLI."""
38
+
39
+ def __init__(self, token_path: str):
40
+ super().__init__()
41
+ self.token_path = token_path
42
+ self.set_auth_status(self.auth.check())
43
+
44
+ # TODO: add scrollbar styling here?
45
+ DEFAULT_CSS = """
46
+ Toast.-error {
47
+ background: $primary;
48
+ }
49
+
50
+ """
51
+
52
+ ENABLE_COMMAND_PALETTE = False # True by default
53
+
54
+ # Keybindings for the app, placed in the footer.
55
+ BINDINGS = [
56
+ Binding("q", "quit", "Quit"),
57
+ Binding("h", "help", "Help"),
58
+ ]
59
+
60
+ def compose(self) -> ComposeResult:
61
+ yield Header(show_clock=True, time_format="%H:%M:%S", icon="")
62
+ yield ProjectView()
63
+ yield DDSFooter()
64
+
65
+ def on_mount(self) -> None:
66
+ """On mount, register the theme and set it as the active theme."""
67
+ self.register_theme(theme)
68
+ self.theme = "custom"
69
+
70
+ def action_help(self) -> None:
71
+ """Action to show the help screen."""
@@ -0,0 +1 @@
1
+ """DDS GUI components package."""