diff --git a/latch/cli/main.py b/latch/cli/main.py index 76a8be2c..1b206c75 100644 --- a/latch/cli/main.py +++ b/latch/cli/main.py @@ -39,10 +39,13 @@ def register(pkg_root: str, dockerfile: Union[str, None], pkg_name: Union[str, N Visit docs.latch.bio to learn more. """ - _register(pkg_root, dockerfile, pkg_name) - click.secho( - "Successfully registered workflow. View @ console.latch.bio.", fg="green" - ) + try: + _register(pkg_root, dockerfile, pkg_name) + click.secho( + "Successfully registered workflow. View @ console.latch.bio.", fg="green" + ) + except Exception as e: + click.secho(f"Unable to register workflow: {str(e)}", fg="red") @click.command("login") @@ -51,8 +54,11 @@ def login(): Visit docs.latch.bio to learn more. """ - _login() - click.secho("Successfully logged into LatchBio.", fg="green") + try: + _login() + click.secho("Successfully logged into LatchBio.", fg="green") + except Exception as e: + click.secho(f"Unable to log in: {str(e)}", fg="red") @click.command("init") @@ -62,7 +68,11 @@ def init(pkg_name: str): Visit docs.latch.bio to learn more. """ - _init(pkg_name) + try: + _init(pkg_name) + except Exception as e: + click.secho(f"Unable to initialize {pkg_name}: {str(e)}", fg="red") + return click.secho(f"Created a latch workflow called {pkg_name}.", fg="green") click.secho("Run", fg="green") click.secho(f"\t$ latch register {pkg_name}", fg="green") @@ -77,9 +87,15 @@ def cp(source_file: str, destination_file: str): Visit docs.latch.bio to learn more. """ - _cp(source_file, destination_file) - click.secho( - f"Successfully copied {source_file} to {destination_file}.", fg="green") + try: + _cp(source_file, destination_file) + click.secho( + f"Successfully copied {source_file} to {destination_file}.", fg="green" + ) + except Exception as e: + click.secho( + f"Unable to copy {source_file} to {destination_file}: {str(e)}", fg="red" + ) @click.command("ls") @@ -90,74 +106,54 @@ def ls(remote_directories: Union[None, List[str]]): Visit docs.latch.bio to learn more. """ - _item_padding = 3 + _item_padding = lambda k: 0 if k == "modifyTime" else 3 # If the user doesn't provide any arguments, default to root if not remote_directories: remote_directories = ["latch:///"] - # conditional formatting based on whether the user asks for multiple ls's or not - _initial_padding = 0 if len(remote_directories) < 2 else 3 - - def _emit_directory_header(remote_directory): - if len(remote_directories) > 1: - click.secho(f"{remote_directory}:") - click.secho("") - - def _emit_directory_footer(): - if len(remote_directories) > 1: - click.secho("") - for remote_directory in remote_directories: - output, max_lengths = _ls(remote_directory, padding=_item_padding) - - header_name_padding = max_lengths["name"] - len("Name") - header_content_type_padding = max_lengths["content_type"] - len("Type") - header_content_size_padding = max_lengths["content_size"] - len("Size") - header_modify_time_padding = max_lengths["modify_time"] - len("Last Modified") - - header = ( - "Name" - + " " * header_name_padding - + "Type" - + " " * header_content_type_padding - + "Size" - + " " * header_content_size_padding - + "Last Modified" - + " " * header_modify_time_padding - ) - - _emit_directory_header(remote_directory=remote_directory) + try: + output = _ls(remote_directory) + except Exception as e: + click.secho( + f"Unable to display contents of {remote_directory}: {str(e)}", fg="red" + ) + continue - click.secho(" " * _initial_padding, nl=False) - click.secho(header, underline=True) + header = { + "name": "Name:", + "contentType": "Type:", + "contentSize": "Size:", + "modifyTime": "Last Modified:", + } + max_lengths = {key: len(key) + _item_padding(key) for key in header} for row in output: - name, t, content_type, content_size, modify_time = row + for key in header: + max_lengths[key] = max( + len(row[key]) + _item_padding(key), max_lengths[key] + ) + + def _display(row, style): + click.secho(f"{row['name']:<{max_lengths['name']}}", nl=False, **style) + click.secho( + f"{row['contentType']:<{max_lengths['contentType']}}", nl=False, **style + ) + click.secho( + f"{row['contentSize']:<{max_lengths['contentSize']}}", nl=False, **style + ) + click.secho(f"{row['modifyTime']}", **style) + + _display(header, style={"underline": True}) + for row in output: style = { - "fg": "cyan" if t == "obj" else "green", + "fg": "cyan" if row["type"] == "obj" else "green", "bold": True, } - name_padding = max_lengths["name"] - len(name) - content_type_padding = max_lengths["content_type"] - len(content_type) - content_size_padding = max_lengths["content_size"] - len(content_size) - - output_str = ( - name - + " " * name_padding - + content_type - + " " * content_type_padding - + content_size - + " " * content_size_padding - + modify_time - ) - - click.secho(" " * _initial_padding, nl=False) - click.secho(output_str, **style) - - _emit_directory_footer() + _display(row, style) @click.command("execute") @@ -172,7 +168,11 @@ def execute(params_file: Path, version: Union[str, None] = None): Visit docs.latch.bio to learn more. """ - wf_name = _execute(params_file, version) + try: + wf_name = _execute(params_file, version) + except Exception as e: + click.secho(f"Unable to execute workflow: {str(e)}", fg="red") + return if version is None: version = "latest" click.secho( @@ -192,7 +192,11 @@ def get_wf(name: Union[str, None] = None): Visit docs.latch.bio to learn more. """ - wfs = _get_wf(name) + try: + wfs = _get_wf(name) + except Exception as e: + click.secho(f"Unable to get workflows: {str(e)}", fg="red") + return id_padding, name_padding, version_padding = 0, 0, 0 for wf in wfs: id, name, version = wf @@ -202,10 +206,12 @@ def get_wf(name: Union[str, None] = None): version_padding = max(version_padding, version_len) click.secho( - f"ID{id_padding * ' '}\tName{name_padding * ' '}\tVersion{version_padding * ' '}") + f"ID{id_padding * ' '}\tName{name_padding * ' '}\tVersion{version_padding * ' '}" + ) for wf in wfs: click.secho( - f"{wf[0]}{(id_padding - len(str(wf[0]))) * ' '}\t{wf[1]}{(name_padding - len(wf[1])) * ' '}\t{wf[2]}{(version_padding - len(wf[2])) * ' '}") + f"{wf[0]}{(id_padding - len(str(wf[0]))) * ' '}\t{wf[1]}{(name_padding - len(wf[1])) * ' '}\t{wf[2]}{(version_padding - len(wf[2])) * ' '}" + ) main.add_command(register) diff --git a/latch/services/execute.py b/latch/services/execute.py index 9ac54e4f..c4260ec3 100644 --- a/latch/services/execute.py +++ b/latch/services/execute.py @@ -15,7 +15,7 @@ from urllib3.util.retry import Retry -def execute(params_file: Path, version: Union[None, str] = None) -> bool: +def execute(params_file: Path, version: Union[None, str] = None) -> str: """Executes a versioned workflow with parameters specified in python. Args: @@ -30,7 +30,7 @@ def execute(params_file: Path, version: Union[None, str] = None) -> bool: `execute` service. Returns: - True if successful + The name of the workflow if successful Example: :: diff --git a/latch/services/ls.py b/latch/services/ls.py index 819c50c5..4839934b 100644 --- a/latch/services/ls.py +++ b/latch/services/ls.py @@ -5,49 +5,36 @@ import requests from latch.utils import retrieve_or_login -def _str_none(s: Optional[str]) -> str: - if s is None: - return "-" - return str(s) -def ls(remote_directory: str, padding: int = 3): +def ls(remote_directory: str): + + if remote_directory.startswith("latch://"): + remote_directory = remote_directory[len("latch://") :] + if not remote_directory.startswith("/"): + remote_directory = f"/{remote_directory}" + url = "https://nucleus.latch.bio/sdk/list" token = retrieve_or_login() headers = {"Authorization": f"Bearer {token}"} data = {"directory": remote_directory} response = requests.post(url, headers=headers, json=data) - json_data = response.json() - - # used for pretty printing - # Initial values are the number of characters in the output header - max_lengths = { - "name": len("Name") + padding, - "content_type": len("Type") + padding, - "content_size": len("Size") + padding, - "modify_time": len("Last Modified"), - } + if response.status_code == 403: + raise ValueError( + "you need access to the latch sdk beta ~ join the waitlist @ https://latch.bio/sdk" + ) + elif response.status_code == 500: + raise ValueError(f"the directory does not exist.") + + json_data = response.json() output = [] for i in json_data: name_data = json_data[i] - name = _str_none(name_data["name"]) - t = _str_none(name_data["type"]) - if t == "dir" and name[-1] != "/": - name = name_data["name"] = f"{name}/" - name_data["content_type"] = "directory" - content_type = _str_none(name_data["content_type"]) - content_size = _str_none(name_data["content_size"]) - modify_time = _str_none(name_data["modify_time"]) - - output.append((name, t, content_type, content_size, modify_time)) - - for i in max_lengths: - _padding = 0 if i == "modify_time" else padding - max_lengths[i] = max(max_lengths[i], _padding + len(_str_none(name_data[i]))) + output.append(name_data) - output.sort() - output.sort(key=lambda x: x[1]) + output.sort(key=lambda x: x["name"]) + output.sort(key=lambda x: x["type"]) - return output, max_lengths + return output diff --git a/tests/.hypothesis/unicode_data/13.0.0/charmap.json.gz b/tests/.hypothesis/unicode_data/13.0.0/charmap.json.gz new file mode 100644 index 00000000..ab3c6b39 Binary files /dev/null and b/tests/.hypothesis/unicode_data/13.0.0/charmap.json.gz differ diff --git a/tests/test_cli.py b/tests/test_cli.py index d5b6877a..5dd66325 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -55,7 +55,7 @@ def test_ls(test_account_jwt): # todo(ayush) add more ls tests _cmd = ["latch", "ls"] - _run_and_verify(_cmd, "welcome/") + _run_and_verify(_cmd, "welcome") def test_execute(test_account_jwt):