Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions datashuttle/configs/canonical_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Copy link
Member

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_path because we are not a local only project (i.e. we can transfer to google drive). Here the central_path could be a folder within the google drive folder. Or, behind the scenes we could set this to empty or blank 🤔

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be is_cloud_config_method for readability in the conditional statements

"""
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]:
Expand Down
8 changes: 8 additions & 0 deletions datashuttle/datashuttle_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ def make_config_file(
connection_method: str | None = None,
central_host_id: Optional[str] = None,
central_host_username: Optional[str] = None,
google_drive_folder_id: Optional[str] = None,
) -> None:
"""
Initialise the configurations for datashuttle to use on the
Expand Down Expand Up @@ -967,6 +968,7 @@ def make_config_file(
"connection_method": connection_method,
"central_host_id": central_host_id,
"central_host_username": central_host_username,
"google_drive_folder_id": google_drive_folder_id,
},
)

Expand Down Expand Up @@ -1459,6 +1461,12 @@ def _setup_rclone_central_ssh_config(self, log: bool) -> None:
log=log,
)

def _setup_rclone_central_google_drive_config(self, log: bool) -> None:
rclone.setup_rclone_clone_for_google_drive(
self.cfg.get_rclone_config_name("google_drive"),
log=log,
)

def _setup_rclone_central_local_filesystem_config(self) -> None:
rclone.setup_rclone_config_for_local_filesystem(
self.cfg.get_rclone_config_name("local_filesystem"),
Expand Down
160 changes: 151 additions & 9 deletions datashuttle/tui/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand All @@ -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"]:
Copy link
Member

Choose a reason for hiding this comment

The 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:
Expand Down Expand Up @@ -327,6 +364,44 @@ def switch_ssh_widgets_display(self, display_ssh: bool) -> None:
placeholder
)

def switch_google_drive_widgets_display(
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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"]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The central_path can be handled here (e.g. allow user to specify path within the google drive folder to save e.g. they might have multiple projects in the same folder).

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:
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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":
Expand All @@ -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 = (
Expand All @@ -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
Expand All @@ -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"

Expand All @@ -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"
Expand Down
Loading
Loading