Skip to content

Commit

Permalink
Merge branch 'main' of github.com:latchbio/latch into ayush/hannah-sm…
Browse files Browse the repository at this point in the history
…-bugs
  • Loading branch information
ayushkamat committed Nov 13, 2023
2 parents 36c99f8 + f012328 commit 6c5f2cc
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 23 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ Types of changes

# Latch SDK Changelog

## 2.36.6 - 2023-11-09

### Added

* Added ability to skip version check using an env variable

## 2.36.5 - 2023-11-08

### Fixed

* Bug in `latch login` where not having a token would prevent token generation

## 2.36.4 - 2023-10-25

### Added
Expand Down
67 changes: 52 additions & 15 deletions latch_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import sys
from pathlib import Path
from textwrap import dedent
from typing import List, Optional, Tuple, Union
from typing import Callable, List, Optional, Tuple, TypeVar, Union

import click
from packaging.version import parse as parse_version
from typing_extensions import ParamSpec

import latch_cli.click_utils
from latch_cli.click_utils import EnumChoice
Expand All @@ -30,6 +31,32 @@

crash_handler = CrashHandler()

P = ParamSpec("P")
T = TypeVar("T")


def requires_login(f: Callable[P, T]) -> Callable[P, T]:
def decorated(*args, **kwargs):
try:
get_auth_header()
except AuthenticationError as e:
click.secho(
dedent("""
Unable to authenticate with Latch.
If you are on a machine with a browser, run `latch login`.
If not, navigate to `https://console.latch.bio/settings/developer` on a different machine, select `Access Tokens`, and copy your `API Key` to `~/.latch/token` on this machine.
If you do not see this value in the console, make sure you are logged into your personal workspace.
""").strip("\n"),
fg="red",
)
raise click.exceptions.Exit(1) from e

return f(*args, **kwargs)

return decorated


@click.group(
"latch",
Expand All @@ -43,19 +70,8 @@ def main():
Collection of command line tools for using the Latch SDK and
interacting with the Latch platform.
"""
try:
get_auth_header()
except AuthenticationError as e:
click.secho(
dedent("""
Unable to authenticate with Latch.
If you are on a machine with a browser, run `latch login`.
If not, navigate to `https://console.latch.bio/settings/developer` on a different machine, select `Access Tokens`, and copy your `API Key` to `~/.latch/token` on this machine.
""").strip("\n"),
fg="red",
)
raise click.exceptions.Exit(1) from e
if os.environ.get("LATCH_SKIP_VERSION_CHECK") is not None:
return

local_ver = parse_version(get_local_package_version())
latest_ver = parse_version(get_latest_package_version())
Expand Down Expand Up @@ -150,6 +166,7 @@ def dockerfile(pkg_root: str, snakemake: bool = False):
default=None,
help="Path to a Snakefile to register.",
)
@requires_login
def register(
pkg_root: str,
disable_auto_version: bool,
Expand Down Expand Up @@ -204,6 +221,7 @@ def register(
type=EnumChoice(TaskSize, case_sensitive=False),
help="Instance size to use for develop session.",
)
@requires_login
def local_development(
pkg_root: Path, yes: bool, image: Optional[str], size: Optional[TaskSize]
):
Expand Down Expand Up @@ -323,6 +341,7 @@ def init(
default=False,
show_default=True,
)
@requires_login
def cp(
src: List[str],
dest: str,
Expand Down Expand Up @@ -357,6 +376,7 @@ def cp(
default=False,
show_default=True,
)
@requires_login
def mv(src: str, dest: str, no_glob: bool):
"""Move remote files in LatchData."""

Expand All @@ -377,6 +397,7 @@ def mv(src: str, dest: str, no_glob: bool):
default=False,
)
@click.argument("paths", nargs=-1, shell_complete=remote_complete)
@requires_login
def ls(paths: Tuple[str], group_directories_first: bool):
"""
List the contents of a Latch Data directory
Expand Down Expand Up @@ -451,6 +472,7 @@ def generate_metadata(
default=None,
help="The version of the workflow to launch. Defaults to latest.",
)
@requires_login
def launch(params_file: Path, version: Union[str, None] = None):
"""Launch a workflow using a python parameter map."""

Expand All @@ -476,6 +498,7 @@ def launch(params_file: Path, version: Union[str, None] = None):
default=None,
help="The version of the workflow. Defaults to latest.",
)
@requires_login
def get_params(wf_name: Union[str, None], version: Union[str, None] = None):
"""Generate a python parameter map for a workflow."""
crash_handler.message = "Unable to generate param map for workflow"
Expand All @@ -499,6 +522,7 @@ def get_params(wf_name: Union[str, None], version: Union[str, None] = None):
default=None,
help="The name of the workflow to list. Will display all versions",
)
@requires_login
def get_wf(name: Union[str, None] = None):
"""List workflows."""
crash_handler.message = "Unable to get workflows"
Expand Down Expand Up @@ -527,6 +551,7 @@ def get_wf(name: Union[str, None] = None):

@main.command("open")
@click.argument("remote_file", nargs=1, type=str)
@requires_login
def open_remote_file(remote_file: str):
"""Open a remote file in the browser."""
crash_handler.message = f"Unable to open {remote_file}"
Expand All @@ -540,6 +565,7 @@ def open_remote_file(remote_file: str):

@main.command("rm")
@click.argument("remote_path", nargs=1, type=str)
@requires_login
def rm(remote_path: str):
"""Deletes a remote entity."""
crash_handler.message = f"Unable to delete {remote_path}"
Expand All @@ -556,6 +582,7 @@ def rm(remote_path: str):

@main.command("mkdir")
@click.argument("remote_directory", nargs=1, type=str)
@requires_login
def mkdir(remote_directory: str):
"""Creates a new remote directory."""
crash_handler.message = f"Unable to create directory {remote_directory}"
Expand All @@ -573,6 +600,7 @@ def mkdir(remote_directory: str):

@main.command("touch")
@click.argument("remote_file", nargs=1, type=str)
@requires_login
def touch(remote_file: str):
"""Creates an empty text file."""
crash_handler.message = f"Unable to create {remote_file}"
Expand All @@ -590,6 +618,7 @@ def touch(remote_file: str):

@main.command("exec")
@click.argument("task_name", nargs=1, type=str)
@requires_login
def execute(task_name: str):
"""Drops the user into an interactive shell from within a task."""
crash_handler.message = f"Unable to exec into {task_name}"
Expand All @@ -602,6 +631,7 @@ def execute(task_name: str):

@main.command("preview")
@click.argument("pkg_root", nargs=1, type=click.Path(exists=True, path_type=Path))
@requires_login
def preview(pkg_root: Path):
"""Creates a preview of your workflow interface."""
crash_handler.message = f"Unable to preview inputs for {pkg_root}"
Expand All @@ -613,6 +643,7 @@ def preview(pkg_root: Path):


@main.command("workspace")
@requires_login
def workspace():
"""Spawns an interactive terminal prompt allowing users to choose what workspace they want to work in."""

Expand All @@ -625,6 +656,7 @@ def workspace():


@main.command("get-executions")
@requires_login
def get_executions():
"""Spawns an interactive terminal UI that shows all executions in a given workspace"""

Expand All @@ -643,6 +675,7 @@ def pods():

@pods.command("stop")
@click.argument("pod_id", nargs=1, type=int, required=False)
@requires_login
def stop_pod(pod_id: Optional[int] = None):
"""Stops a pod given a pod_id or the pod from which the command is run"""
crash_handler.message = "Unable to stop pod"
Expand Down Expand Up @@ -693,6 +726,7 @@ def test_data(ctx: click.Context):
type=bool,
help="Automatically overwrite any files without asking for confirmation.",
)
@requires_login
def test_data_upload(src_path: str, dont_confirm_overwrite: bool):
"""Upload test data object."""

Expand All @@ -707,6 +741,7 @@ def test_data_upload(src_path: str, dont_confirm_overwrite: bool):

@test_data.command("remove")
@click.argument("object_url", nargs=1, type=str)
@requires_login
def test_data_remove(object_url: str):
"""Remove test data object."""

Expand All @@ -720,6 +755,7 @@ def test_data_remove(object_url: str):


@test_data.command("ls")
@requires_login
def test_data_ls():
"""List test data objects."""

Expand All @@ -734,7 +770,7 @@ def test_data_ls():
print(f"\ts3://latch-public/{o}")


@main.command()
@main.command("sync")
@click.argument("srcs", nargs=-1)
@click.argument("dst", nargs=1)
@click.option(
Expand All @@ -751,6 +787,7 @@ def test_data_ls():
is_flag=True,
default=False,
)
@requires_login
def sync(srcs: List[str], dst: str, delete: bool, ignore_unsyncable: bool):
"""
Update the contents of a remote directory with local data or vice versa.
Expand Down
19 changes: 13 additions & 6 deletions latch_cli/snakemake/config/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from latch_cli.snakemake.workflow import reindent
from latch_cli.utils import identifier_from_str

from ..serialize_utils import best_effort_display_name
from .utils import JSONValue, get_preamble, parse_type, parse_value, type_repr

T = TypeVar("T")
Expand Down Expand Up @@ -81,10 +82,11 @@ def generate_metadata(

is_file = typ in {LatchFile, LatchDir}
param_typ = "SnakemakeFileParameter" if is_file else "SnakemakeParameter"

param_str = reindent(
f"""\
{repr(identifier_from_str(k))}: {param_typ}(
display_name={repr(k)},
display_name={repr(best_effort_display_name(k))},
type={type_repr(typ)},
__config____default__),""",
0,
Expand Down Expand Up @@ -126,14 +128,19 @@ def generate_metadata(
old_metadata_path.rename(metadata_path)
elif old_metadata_path.exists() and metadata_path.exists():
click.secho(
"Warning: Found both `latch_metadata.py` and"
" `latch_metadata/__init__.py` in current directory."
" `latch_metadata.py` will be ignored.",
(
"Warning: Found both `latch_metadata.py` and"
" `latch_metadata/__init__.py` in current directory."
" `latch_metadata.py` will be ignored."
),
fg="yellow",
)

if not metadata_path.exists() and click.confirm(
"Could not find an `__init__.py` file in `latch_metadata`. Generate one?"
"Could not find an `__init__.py` file in `latch_metadata`. This file"
"defines the metadata object that configures your interface and "
"uses parameters imported from `parameters.py`"
"Generate one?"
):
metadata_path.write_text(
reindent(
Expand Down Expand Up @@ -182,7 +189,7 @@ def generate_metadata(
# Import these into your `__init__.py` file:
#
# from .parameters import generated_parameters
#
generated_parameters = {
__params__
}
Expand Down
3 changes: 2 additions & 1 deletion latch_cli/snakemake/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,8 @@ def generate_jit_register_code(
from functools import partial
from pathlib import Path
import shutil
from typing import List, NamedTuple, Optional, TypedDict, Dict
import typing
from typing import NamedTuple, Optional, TypedDict, Dict
import hashlib
from urllib.parse import urljoin
from dataclasses import is_dataclass, asdict
Expand Down
8 changes: 8 additions & 0 deletions latch_cli/snakemake/serialize_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from pathlib import Path
from typing import Dict, Union

Expand Down Expand Up @@ -222,3 +223,10 @@ def update_mapping(cur: Path, stem: Path, remote: str, mapping: Dict[str, str]):

for p in cur.iterdir():
update_mapping(p, stem / p.name, urljoins(remote, p.name), mapping)


underscores = re.compile(r"_+")


def best_effort_display_name(x: str):
return underscores.sub(" ", x).title().strip()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

setup(
name="latch",
version="v2.36.4",
version="v2.36.6",
author_email="[email protected]",
description="The Latch SDK",
packages=find_packages(),
Expand Down

0 comments on commit 6c5f2cc

Please sign in to comment.