-
Notifications
You must be signed in to change notification settings - Fork 38
feat: Allow Google Drive as remove storage #502
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
920271e
b5119bb
2ca7875
c247299
4da8c65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,9 +39,12 @@ def get_canonical_configs() -> dict: | |
| canonical_configs = { | ||
| "local_path": Union[str, Path], | ||
| "central_path": Optional[Union[str, Path]], | ||
| "connection_method": Optional[Literal["ssh", "local_filesystem"]], | ||
| "connection_method": Optional[ | ||
| Literal["ssh", "local_filesystem", "google_drive"] | ||
| ], | ||
| "central_host_id": Optional[str], | ||
| "central_host_username": Optional[str], | ||
| "google_drive_folder_id": Optional[str], | ||
| } | ||
|
|
||
| return canonical_configs | ||
|
|
@@ -148,15 +151,25 @@ def raise_on_bad_local_only_project_configs(config_dict: Configs) -> None: | |
| should be set and not the other. Either both are set ('full' project) or | ||
| neither are ('local only' project). Check this assumption here. | ||
| """ | ||
| params_are_none = local_only_configs_are_none(config_dict) | ||
| cloud_method = cloud_config_method(config_dict) | ||
|
|
||
| if any(params_are_none): | ||
| if not all(params_are_none): | ||
| utils.log_and_raise_error( | ||
| "Either both `central_path` and `connection_method` must be set, " | ||
| "or must both be `None` (for local-project mode).", | ||
| ConfigError, | ||
| ) | ||
| if not cloud_method: | ||
| params_are_none = local_only_configs_are_none(config_dict) | ||
|
|
||
| if any(params_are_none): | ||
| if not all(params_are_none): | ||
| utils.log_and_raise_error( | ||
| "Either both `central_path` and `connection_method` must be set, " | ||
| "or must both be `None` (for local-project mode).", | ||
| ConfigError, | ||
| ) | ||
|
|
||
|
|
||
| def cloud_config_method(config_dict: Configs) -> bool: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could be |
||
| """ | ||
| Check if the config is set to Google Drive or AWS. | ||
| """ | ||
| return config_dict["connection_method"] in ["google_drive", "aws"] | ||
|
|
||
|
|
||
| def local_only_configs_are_none(config_dict: Configs) -> list[bool]: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,11 @@ | |
|
|
||
| from datashuttle.tui.custom_widgets import ClickableInput | ||
| from datashuttle.tui.interface import Interface | ||
| from datashuttle.tui.screens import modal_dialogs, setup_ssh | ||
| from datashuttle.tui.screens import ( | ||
| modal_dialogs, | ||
| setup_google_drive, | ||
| setup_ssh, | ||
| ) | ||
| from datashuttle.tui.tooltips import get_tooltip | ||
|
|
||
|
|
||
|
|
@@ -58,6 +62,7 @@ def __init__( | |
| self.parent_class = parent_class | ||
| self.interface = interface | ||
| self.config_ssh_widgets: List[Any] = [] | ||
| self.config_google_drive_widgets: List[Any] = [] | ||
|
|
||
| def compose(self) -> ComposeResult: | ||
| """ | ||
|
|
@@ -90,6 +95,18 @@ def compose(self) -> ComposeResult: | |
| ), | ||
| ] | ||
|
|
||
| self.config_google_drive_widgets = [ | ||
| Label( | ||
| "Google Drive Folder ID", | ||
| id="configs_google_drive_folder_id_label", | ||
| ), | ||
| ClickableInput( | ||
| self.parent_class.mainwindow, | ||
| placeholder="e.g. 1zQq4t8l2f4g5h6j7k8l9m0n1o2p3q4r5s6t7u8v9w0x", | ||
| id="configs_google_drive_folder_id_input", | ||
| ), | ||
| ] | ||
|
|
||
| config_screen_widgets = [ | ||
| Label("Local Path", id="configs_local_path_label"), | ||
| Horizontal( | ||
|
|
@@ -108,13 +125,17 @@ def compose(self) -> ComposeResult: | |
| id="configs_local_filesystem_radiobutton", | ||
| ), | ||
| RadioButton("SSH", id="configs_ssh_radiobutton"), | ||
| RadioButton( | ||
| "Google Drive", id="configs_google_drive_radiobutton" | ||
| ), | ||
| RadioButton( | ||
| "No connection (local only)", | ||
| id="configs_local_only_radiobutton", | ||
| ), | ||
| id="configs_connect_method_radioset", | ||
| ), | ||
| *self.config_ssh_widgets, | ||
| *self.config_google_drive_widgets, | ||
| Label("Central Path", id="configs_central_path_label"), | ||
| Horizontal( | ||
| ClickableInput( | ||
|
|
@@ -131,6 +152,10 @@ def compose(self) -> ComposeResult: | |
| "Setup SSH Connection", | ||
| id="configs_setup_ssh_connection_button", | ||
| ), | ||
| Button( | ||
| "Setup Google Drive Connection", | ||
| id="configs_setup_google_drive_connection_button", | ||
| ), | ||
| # Below button is always hidden when accessing | ||
| # configs from project manager screen | ||
| Button( | ||
|
|
@@ -189,9 +214,15 @@ def on_mount(self) -> None: | |
| True | ||
| ) | ||
| self.switch_ssh_widgets_display(display_ssh=False) | ||
| self.switch_google_drive_widgets_display( | ||
| display_google_drive=False | ||
| ) | ||
| self.query_one("#configs_setup_ssh_connection_button").visible = ( | ||
| False | ||
| ) | ||
| self.query_one( | ||
| "#configs_setup_google_drive_connection_button" | ||
| ).visible = False | ||
|
|
||
| # Setup tooltips | ||
| if not self.interface: | ||
|
|
@@ -215,9 +246,11 @@ def on_mount(self) -> None: | |
| "#configs_connect_method_label", | ||
| "#configs_local_filesystem_radiobutton", | ||
| "#configs_ssh_radiobutton", | ||
| "#configs_google_drive_radiobutton", | ||
| "#configs_local_only_radiobutton", | ||
| "#configs_central_host_username_input", | ||
| "#configs_central_host_id_input", | ||
| "#configs_google_drive_folder_id_input", | ||
| ]: | ||
| self.query_one(id).tooltip = get_tooltip(id) | ||
|
|
||
|
|
@@ -235,25 +268,29 @@ def on_radio_set_changed(self, event: RadioSet.Changed) -> None: | |
| label = str(event.pressed.label) | ||
| assert label in [ | ||
| "SSH", | ||
| "Google Drive", | ||
| "Local Filesystem", | ||
| "No connection (local only)", | ||
| ], "Unexpected label." | ||
|
|
||
| if label == "No connection (local only)": | ||
| if label in ["No connection (local only)", "Google Drive"]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is out of scope for this PR, but something to think about. This class is growing and handling more cases, so we keep having to add extra conditionals. This is usually a sign the class is doing too much and should be refactored. In future, we could think about how to abstract away the connection method. Also, the string compares I had are a bit of a hack 😅 these could be converted to enums in future. |
||
| self.query_one("#configs_central_path_input").value = "" | ||
| self.query_one("#configs_central_path_input").disabled = True | ||
| self.query_one("#configs_central_path_select_button").disabled = ( | ||
| True | ||
| ) | ||
| display_ssh = False | ||
| display_google_drive = label == "Google Drive" | ||
| else: | ||
| self.query_one("#configs_central_path_input").disabled = False | ||
| self.query_one("#configs_central_path_select_button").disabled = ( | ||
| False | ||
| ) | ||
| display_ssh = True if label == "SSH" else False | ||
| display_google_drive = False | ||
|
|
||
| self.switch_ssh_widgets_display(display_ssh) | ||
| self.switch_google_drive_widgets_display(display_google_drive) | ||
| self.set_central_path_input_tooltip(display_ssh) | ||
|
|
||
| def set_central_path_input_tooltip(self, display_ssh: bool) -> None: | ||
|
|
@@ -327,6 +364,44 @@ def switch_ssh_widgets_display(self, display_ssh: bool) -> None: | |
| placeholder | ||
| ) | ||
|
|
||
| def switch_google_drive_widgets_display( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, all this logic is neatly wrapped into this clean function |
||
| self, display_google_drive: bool | ||
| ) -> None: | ||
| """ | ||
| Show or hide Google Drive-related configs based on whether the current | ||
| `connection_method` widget is "google_drive" or "local_filesystem". | ||
|
|
||
| Parameters | ||
| ---------- | ||
|
|
||
| display_google_drive : bool | ||
| If `True`, display the Google Drive-related widgets. | ||
| """ | ||
| for widget in self.config_google_drive_widgets: | ||
| widget.display = display_google_drive | ||
|
|
||
| # Setting all the central path widgets to not display | ||
| self.query_one("#configs_central_path_select_button").display = ( | ||
| not display_google_drive | ||
| ) | ||
|
|
||
| self.query_one("#configs_central_path_input").display = ( | ||
| not display_google_drive | ||
| ) | ||
|
|
||
| self.query_one("#configs_central_path_label").display = ( | ||
| not display_google_drive | ||
| ) | ||
|
|
||
| if self.interface is None: | ||
| self.query_one( | ||
| "#configs_setup_google_drive_connection_button" | ||
| ).visible = False | ||
| else: | ||
| self.query_one( | ||
| "#configs_setup_google_drive_connection_button" | ||
| ).visible = display_google_drive | ||
|
|
||
| def on_button_pressed(self, event: Button.Pressed) -> None: | ||
| """ | ||
| Enables the Create Folders button to read out current input values | ||
|
|
@@ -341,6 +416,9 @@ def on_button_pressed(self, event: Button.Pressed) -> None: | |
| elif event.button.id == "configs_setup_ssh_connection_button": | ||
| self.setup_ssh_connection() | ||
|
|
||
| elif event.button.id == "configs_setup_google_drive_connection_button": | ||
| self.setup_google_drive_connection() | ||
|
|
||
| elif event.button.id == "configs_go_to_project_screen_button": | ||
| self.parent_class.dismiss(self.interface) | ||
|
|
||
|
|
@@ -409,6 +487,23 @@ def setup_ssh_connection(self) -> None: | |
| setup_ssh.SetupSshScreen(self.interface) | ||
| ) | ||
|
|
||
| def setup_google_drive_connection(self) -> None: | ||
| """ | ||
| Set up the `SetupGoogleDriveScreen` screen, | ||
| """ | ||
| assert self.interface is not None, "type narrow flexible `interface`" | ||
|
|
||
| if not self.widget_configs_match_saved_configs(): | ||
| self.parent_class.mainwindow.show_modal_error_dialog( | ||
| "The values set above must equal the datashuttle settings. " | ||
| "Either press 'Save' or reload this page." | ||
| ) | ||
| return | ||
|
|
||
| self.parent_class.mainwindow.push_screen( | ||
| setup_google_drive.SetupGoogleDriveScreen(self.interface) | ||
| ) | ||
|
|
||
| def widget_configs_match_saved_configs(self): | ||
| """ | ||
| Check that the configs currently stored in the widgets | ||
|
|
@@ -423,13 +518,17 @@ def widget_configs_match_saved_configs(self): | |
|
|
||
| project_name = self.interface.project.cfg.project_name | ||
|
|
||
| for key, value in cfg_kwargs.items(): | ||
| saved_val = self.interface.get_configs()[key] | ||
| if key in ["central_path", "local_path"]: | ||
| if value.name != project_name: | ||
| value = value / project_name | ||
| if saved_val != value: | ||
| return False | ||
| connection_method = cfg_kwargs["connection_method"] | ||
|
|
||
| if connection_method not in ["google_drive"]: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| for key, value in cfg_kwargs.items(): | ||
| saved_val = self.interface.get_configs()[key] | ||
| if key in ["central_path", "local_path"]: | ||
| if value.name != project_name: | ||
| value = value / project_name | ||
| if saved_val != value: | ||
| return False | ||
| return True | ||
| return True | ||
|
|
||
| def setup_configs_for_a_new_project(self) -> None: | ||
|
|
@@ -478,6 +577,23 @@ def setup_configs_for_a_new_project(self) -> None: | |
| "able to create and transfer project folders." | ||
| ) | ||
|
|
||
| elif cfg_kwargs["connection_method"] == "google_drive": | ||
|
|
||
| self.query_one( | ||
| "#configs_setup_google_drive_connection_button" | ||
| ).visible = True | ||
| self.query_one( | ||
| "#configs_setup_google_drive_connection_button" | ||
| ).disabled = False | ||
|
|
||
| message = ( | ||
| "A datashuttle project has now been created.\n\n " | ||
| "Next setup the Google Drive connection. " | ||
| "Once complete, navigate to the 'Main Menu' and proceed to the " | ||
| "project page, where you will be able to create " | ||
| "and transfer project folders." | ||
| ) | ||
|
|
||
| else: | ||
| message = ( | ||
| "A datashuttle project has now been created.\n\n " | ||
|
|
@@ -554,6 +670,8 @@ def fill_widgets_with_project_configs(self) -> None: | |
| what_radiobuton_is_on = { | ||
| "configs_ssh_radiobutton": | ||
| cfg_to_load["connection_method"] == "ssh", | ||
| "configs_google_drive_radiobutton": | ||
| cfg_to_load["connection_method"] == "google_drive", | ||
| "configs_local_filesystem_radiobutton": | ||
| cfg_to_load["connection_method"] == "local_filesystem", | ||
| "configs_local_only_radiobutton": | ||
|
|
@@ -568,6 +686,12 @@ def fill_widgets_with_project_configs(self) -> None: | |
| display_ssh=what_radiobuton_is_on["configs_ssh_radiobutton"] | ||
| ) | ||
|
|
||
| self.switch_google_drive_widgets_display( | ||
| display_google_drive=what_radiobuton_is_on[ | ||
| "configs_google_drive_radiobutton" | ||
| ] | ||
| ) | ||
|
|
||
| # Central Host ID | ||
| input = self.query_one("#configs_central_host_id_input") | ||
| value = ( | ||
|
|
@@ -586,6 +710,15 @@ def fill_widgets_with_project_configs(self) -> None: | |
| ) | ||
| input.value = value | ||
|
|
||
| # Google Drive Folder ID | ||
| input = self.query_one("#configs_google_drive_folder_id_input") | ||
| value = ( | ||
| "" | ||
| if cfg_to_load["google_drive_folder_id"] is None | ||
| else cfg_to_load["google_drive_folder_id"] | ||
| ) | ||
| input.value = value | ||
|
|
||
| def get_datashuttle_inputs_from_widgets(self) -> Dict: | ||
| """ | ||
| Get the configs to pass to `make_config_file()` from | ||
|
|
@@ -608,6 +741,9 @@ def get_datashuttle_inputs_from_widgets(self) -> Dict: | |
| if self.query_one("#configs_ssh_radiobutton").value: | ||
| connection_method = "ssh" | ||
|
|
||
| elif self.query_one("#configs_google_drive_radiobutton").value: | ||
| connection_method = "google_drive" | ||
|
|
||
| elif self.query_one("#configs_local_filesystem_radiobutton").value: | ||
| connection_method = "local_filesystem" | ||
|
|
||
|
|
@@ -619,9 +755,15 @@ def get_datashuttle_inputs_from_widgets(self) -> Dict: | |
| central_host_id = self.query_one( | ||
| "#configs_central_host_id_input" | ||
| ).value | ||
| google_drive_folder_id = self.query_one( | ||
| "#configs_google_drive_folder_id_input" | ||
| ).value | ||
| cfg_kwargs["central_host_id"] = ( | ||
| None if central_host_id == "" else central_host_id | ||
| ) | ||
| cfg_kwargs["google_drive_folder_id"] = ( | ||
| None if google_drive_folder_id == "" else google_drive_folder_id | ||
| ) | ||
|
|
||
| central_host_username = self.query_one( | ||
| "#configs_central_host_username_input" | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, we do need to use the
central_pathbecause we are not a local only project (i.e. we can transfer to google drive). Here thecentral_pathcould be a folder within the google drive folder. Or, behind the scenes we could set this to empty or blank 🤔