Skip to content

Commit be34693

Browse files
authored
Merge pull request #587 from latchbio/tim/ws
add better ws handling
2 parents 514e232 + 2d45a18 commit be34693

5 files changed

Lines changed: 65 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ Types of changes
1616

1717
# Latch SDK Changelog
1818

19+
## 2.71.1 - 2026-04-21
20+
21+
### Fixed
22+
23+
* Improve workspace CLI handling when no workspace is selected or available
24+
1925
## 2.71.0 - 2026-04-02
2026

2127
### Dependencies

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ include = ["src/**/*.py", "src/**/py.typed", "src/latch_cli/services/init/*"]
1212

1313
[project]
1414
name = "latch"
15-
version = "2.71.0"
15+
version = "2.71.1"
1616
description = "The Latch SDK"
1717
authors = [{ name = "Kenny Workman", email = "kenny@latch.bio" }]
1818
maintainers = [{ name = "Ayush Kamat", email = "ayush@latch.bio" }]

src/latch/utils.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class WSInfo(TypedDict):
4747
default: bool
4848

4949

50+
class NoWorkspaceSelectedError(ValueError): ...
51+
52+
5053
def get_workspaces() -> Dict[str, WSInfo]:
5154
"""Retrieve workspaces that user can access.
5255
@@ -150,7 +153,7 @@ def current_workspace() -> str:
150153
"""
151154

152155
ws = os.environ.get("LATCH_WORKSPACE")
153-
if ws is not None:
156+
if ws not in {None, ""}:
154157
return ws
155158

156159
ws = user_config.workspace_id
@@ -170,13 +173,20 @@ def current_workspace() -> str:
170173
"""),
171174
)["accountInfoCurrent"]
172175

173-
ws = res["id"]
176+
if res is not None:
177+
is_local = os.environ.get("FLYTE_INTERNAL_EXECUTION_ID") is None
178+
if is_local and res["user"] is not None:
179+
default_account = res["user"]["defaultAccount"]
180+
if default_account is not None:
181+
return default_account
174182

175-
is_local = os.environ.get("FLYTE_INTERNAL_EXECUTION_ID") is None
176-
if is_local and res["user"] is not None:
177-
ws = res["user"]["defaultAccount"]
183+
ws = res["id"]
184+
if ws is not None:
185+
return ws
178186

179-
return ws
187+
raise NoWorkspaceSelectedError(
188+
"No workspaces found. Please create a workspace at https://console.latch.bio before proceeding."
189+
)
180190

181191

182192
class NotFoundError(ValueError): ...

src/latch_cli/main.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import latch_cli.click_utils
1717
from latch.ldata._transfer.progress import Progress as _Progress # noqa: PLC2701
18-
from latch.utils import current_workspace
18+
from latch.utils import NoWorkspaceSelectedError, current_workspace
1919
from latch_cli.click_utils import EnumChoice
2020
from latch_cli.exceptions.handler import CrashHandler
2121
from latch_cli.services.cp.autocomplete import complete as cp_complete
@@ -66,6 +66,21 @@ def decorated(*args: P.args, **kwargs: P.kwargs):
6666
return decorated
6767

6868

69+
def requires_workspace(f: Callable[P, T]) -> Callable[P, T]:
70+
def decorated(*args: P.args, **kwargs: P.kwargs):
71+
try:
72+
current_workspace()
73+
except NoWorkspaceSelectedError as e:
74+
click.secho(str(e), fg="red")
75+
raise click.exceptions.Exit(1) from e
76+
77+
return f(*args, **kwargs)
78+
79+
decorated.__doc__ = f.__doc__
80+
81+
return decorated
82+
83+
6984
@click.group("latch", context_settings={"max_content_width": 160})
7085
@click.version_option(package_name="latch")
7186
def main():
@@ -485,6 +500,7 @@ def generate_metadata(
485500
help="Size of machine to provision for develop session",
486501
)
487502
@requires_login
503+
@requires_workspace
488504
def local_development(
489505
pkg_root: Path,
490506
yes: bool,
@@ -533,6 +549,7 @@ def local_development(
533549
help="Optional container index to inspect (only used for Map Tasks)",
534550
)
535551
@requires_login
552+
@requires_workspace
536553
def execute(
537554
execution_id: Optional[str], egn_id: Optional[str], container_index: Optional[int]
538555
):
@@ -714,6 +731,7 @@ def image_ls():
714731
),
715732
)
716733
@requires_login
734+
@requires_workspace
717735
def register(
718736
pkg_root: str,
719737
disable_auto_version: bool,
@@ -810,6 +828,7 @@ def register(
810828
help="The version of the workflow to launch. Defaults to latest.",
811829
)
812830
@requires_login
831+
@requires_workspace
813832
def launch(params_file: Path, version: Union[str, None] = None):
814833
"""[DEPRECATED] Launch a workflow using a python parameter map.
815834
@@ -851,6 +870,7 @@ def launch(params_file: Path, version: Union[str, None] = None):
851870
"--version", default=None, help="The version of the workflow. Defaults to latest."
852871
)
853872
@requires_login
873+
@requires_workspace
854874
def get_params(wf_name: Union[str, None], version: Union[str, None] = None):
855875
"""[DEPRECATED] Generate a python parameter map for a workflow.
856876
@@ -890,6 +910,7 @@ def get_params(wf_name: Union[str, None], version: Union[str, None] = None):
890910
help="The name of the workflow to list. Will display all versions",
891911
)
892912
@requires_login
913+
@requires_workspace
893914
def get_wf(name: Union[str, None] = None):
894915
"""List workflows."""
895916
crash_handler.message = "Unable to get workflows"
@@ -919,6 +940,7 @@ def get_wf(name: Union[str, None] = None):
919940
@main.command("preview")
920941
@click.argument("pkg_root", nargs=1, type=click.Path(exists=True, path_type=Path))
921942
@requires_login
943+
@requires_workspace
922944
def preview(pkg_root: Path):
923945
"""Creates a preview of your workflow interface."""
924946
crash_handler.message = f"Unable to preview inputs for {pkg_root}"
@@ -931,6 +953,7 @@ def preview(pkg_root: Path):
931953

932954
@main.command("get-executions")
933955
@requires_login
956+
@requires_workspace
934957
def get_executions():
935958
"""Spawns an interactive terminal UI that shows all executions in a given workspace"""
936959

@@ -1263,6 +1286,7 @@ def generate_entrypoint(
12631286
"--execution-id", "-e", type=str, help="Optional execution ID to inspect."
12641287
)
12651288
@requires_login
1289+
@requires_workspace
12661290
def attach(execution_id: Optional[str]):
12671291
"""Drops the user into an interactive shell to inspect the workdir of a nextflow execution."""
12681292

@@ -1302,6 +1326,7 @@ def attach(execution_id: Optional[str]):
13021326
help="Path to the entrypoint nextflow file. Must be relative to the package root.",
13031327
)
13041328
@requires_login
1329+
@requires_workspace
13051330
def nf_register(
13061331
pkg_root: Path,
13071332
yes: bool,

src/latch_cli/services/workspace.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
import click
44
from latch_sdk_config.user import user_config
55

6-
from latch.utils import current_workspace, get_workspaces, WSInfo
6+
from latch.utils import (
7+
NoWorkspaceSelectedError,
8+
WSInfo,
9+
current_workspace,
10+
get_workspaces,
11+
)
712
from latch_cli.menus import SelectOption, select_tui
813

914

@@ -15,7 +20,11 @@ def workspace():
1520
"""
1621
data = get_workspaces()
1722

18-
old_id = current_workspace()
23+
old_id: str | None
24+
try:
25+
old_id = current_workspace()
26+
except NoWorkspaceSelectedError:
27+
old_id = None
1928

2029
selected_marker = "\x1b[3m\x1b[2m (currently selected) \x1b[22m\x1b[23m"
2130

@@ -25,7 +34,11 @@ def workspace():
2534
):
2635
options.append(
2736
{
28-
"display_name": info["name"] if old_id != info["workspace_id"] else info["name"] + selected_marker,
37+
"display_name": (
38+
info["name"]
39+
if old_id != info["workspace_id"]
40+
else info["name"] + selected_marker
41+
),
2942
"value": info,
3043
}
3144
)

0 commit comments

Comments
 (0)