Skip to content

Commit 778cf7a

Browse files
authored
Merge pull request #565 from latchbio/ayush/moar-fix
various fixes
2 parents 04ebe01 + 576a622 commit 778cf7a

File tree

8 files changed

+133
-52
lines changed

8 files changed

+133
-52
lines changed

CHANGELOG.md

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

1717
# Latch SDK Changelog
1818

19+
## 2.67.1 - 2025-08-25
20+
21+
### Added
22+
23+
* `latch dockerfile` now has two new options, `--output` and `--config-path` for choosing where to write the output Dockerfile and where to read/write the `.latch/config` used to generate the dockerfile
24+
* `latch nextflow generate-entrypoint` option `--destination` is now named `--output`
25+
26+
### Changes
27+
28+
* `latch dockerfile` now generates a `.dockerignore` always without asking for confirmation.
29+
1930
## 2.67.0 - 2025-08-21
2031

2132
### Added

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.67.0"
15+
version = "2.67.1"
1616
description = "The Latch SDK"
1717
authors = [{ name = "Kenny Workman", email = "[email protected]" }]
1818
maintainers = [{ name = "Ayush Kamat", email = "[email protected]" }]

src/latch_cli/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
class Units(int, Enum):
77
KiB = 2**10
8-
kB = 10**3
8+
kB = 10**3 # noqa: N815
99

1010
MiB = 2**20
1111
MB = 10**6
@@ -22,7 +22,7 @@ class LatchConstants:
2222
base_image: str = (
2323
"812206152185.dkr.ecr.us-west-2.amazonaws.com/latch-base:cb01-main"
2424
)
25-
nextflow_latest_version: str = "v2.3.0"
25+
nextflow_latest_version: str = "v3.0.5"
2626

2727
file_max_size: int = 4 * Units.MiB
2828
file_chunk_size: int = 64 * Units.MiB

src/latch_cli/docker_utils/__init__.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,8 @@ def generate(self, *, dest: Optional[Path] = None, overwrite: bool = False):
399399

400400

401401
def generate_dockerignore(
402-
pkg_root: Path, *, wf_type: WorkflowType, overwrite: bool = False
402+
dest: Path, *, wf_type: WorkflowType, overwrite: bool = False
403403
) -> None:
404-
dest = Path(pkg_root) / ".dockerignore"
405404
if dest.exists():
406405
if dest.is_dir():
407406
click.secho(
@@ -415,7 +414,7 @@ def generate_dockerignore(
415414
):
416415
return
417416

418-
files = [".git", ".github"]
417+
files = [".git", ".github", ".venv"]
419418

420419
if wf_type == WorkflowType.nextflow:
421420
files.extend([".nextflow*", ".nextflow.log*", "work/", "results/"])
@@ -425,11 +424,13 @@ def generate_dockerignore(
425424
click.secho(f"Successfully generated .dockerignore `{dest}`", fg="green")
426425

427426

428-
def get_default_dockerfile(pkg_root: Path, *, wf_type: WorkflowType, overwrite: bool = False):
427+
def get_default_dockerfile(
428+
pkg_root: Path, *, wf_type: WorkflowType, overwrite: bool = False
429+
):
429430
default_dockerfile = pkg_root / "Dockerfile"
430431

431432
config = get_or_create_workflow_config(
432-
pkg_root=pkg_root,
433+
pkg_root / ".latch" / "config",
433434
base_image_type=BaseImageOptions.nextflow
434435
if wf_type == WorkflowType.nextflow
435436
else BaseImageOptions.default,

src/latch_cli/main.py

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,21 @@ def init(
239239
type=click.Path(exists=True, dir_okay=False, path_type=Path),
240240
help="Path to a direnv file (.env) containing environment variables to inject into the container.",
241241
)
242+
@click.option(
243+
"--output",
244+
"-o",
245+
type=click.Path(path_type=Path),
246+
help="Where to write the result Dockerfile. Default is Dockerfile in the root of the workflow directory.",
247+
)
248+
@click.option(
249+
"--config-path",
250+
type=click.Path(path_type=Path),
251+
help=(
252+
"Where to read the config to use for generating the Dockerfile. If a config is not found either at"
253+
" `config_path` or `config_path / .latch / config`, one will be generated at "
254+
"`config_path / .latch / config`. If not provided, it will default to the parent of the output Dockerfile"
255+
),
256+
)
242257
def dockerfile(
243258
pkg_root: Path,
244259
snakemake: bool = False,
@@ -250,8 +265,10 @@ def dockerfile(
250265
pyproject: Optional[Path] = None,
251266
pip_requirements: Optional[Path] = None,
252267
direnv: Optional[Path] = None,
268+
output: Optional[Path] = None,
269+
config_path: Optional[Path] = None,
253270
):
254-
"""Generates a user editable dockerfile for a workflow and saves under `pkg_root/Dockerfile`.
271+
"""Generates a user editable dockerfile for a workflow.
255272
256273
Visit docs.latch.bio to learn more.
257274
"""
@@ -275,10 +292,32 @@ def dockerfile(
275292
workflow_type = WorkflowType.nextflow
276293
base_image = BaseImageOptions.nextflow
277294

278-
config = get_or_create_workflow_config(
279-
pkg_root=pkg_root, base_image_type=base_image
295+
if output is None:
296+
output = pkg_root / "Dockerfile"
297+
if output.name != "Dockerfile":
298+
output /= "Dockerfile"
299+
300+
ignore_path = output.with_name(".dockerignore")
301+
302+
if config_path is None:
303+
config_path = output.parent / ".latch" / "config"
304+
if config_path.name != "config":
305+
config_path /= ".latch/config"
306+
307+
click.secho(
308+
dedent(f"""\
309+
The following files will be generated:
310+
{click.style("Dockerfile:", fg="bright_blue")} {output}
311+
{click.style("Ignore File:", fg="bright_blue")} {ignore_path}
312+
{click.style("Latch Config:", fg="bright_blue")} {config_path}
313+
""")
280314
)
281315

316+
output.parent.mkdir(exist_ok=True, parents=True)
317+
318+
# todo(ayush): make overwriting this easier
319+
config = get_or_create_workflow_config(config_path, base_image_type=base_image)
320+
282321
builder = DockerfileBuilder(
283322
pkg_root,
284323
wf_type=workflow_type,
@@ -290,12 +329,9 @@ def dockerfile(
290329
pip_requirements=pip_requirements,
291330
direnv=direnv,
292331
)
293-
builder.generate(overwrite=force)
332+
builder.generate(dest=output, overwrite=force)
294333

295-
if not click.confirm("Generate a .dockerignore?"):
296-
return
297-
298-
generate_dockerignore(pkg_root, wf_type=workflow_type, overwrite=force)
334+
generate_dockerignore(ignore_path, wf_type=workflow_type, overwrite=force)
299335

300336

301337
@main.command("generate-metadata")
@@ -1086,8 +1122,8 @@ def version(pkg_root: Path):
10861122
help="Set execution profile for Nextflow workflow",
10871123
)
10881124
@click.option(
1089-
"--destination",
1090-
"-d",
1125+
"--output",
1126+
"-o",
10911127
type=click.Path(path_type=Path),
10921128
default=None,
10931129
help=(
@@ -1101,7 +1137,7 @@ def generate_entrypoint(
11011137
metadata_root: Optional[Path],
11021138
nf_script: Path,
11031139
execution_profile: Optional[str],
1104-
destination: Optional[Path],
1140+
output: Optional[Path],
11051141
yes: bool,
11061142
):
11071143
"""Generate a `wf/entrypoint.py` file from a Nextflow workflow"""
@@ -1110,23 +1146,23 @@ def generate_entrypoint(
11101146
from latch_cli.nextflow.workflow import generate_nextflow_workflow
11111147
from latch_cli.services.register.utils import import_module_by_path
11121148

1113-
if destination is None:
1114-
destination = pkg_root / "wf" / "custom_entrypoint.py"
1149+
if output is None:
1150+
output = pkg_root / "wf" / "custom_entrypoint.py"
11151151

1116-
destination = destination.with_suffix(".py")
1152+
output = output.with_suffix(".py")
11171153

11181154
if not yes and not click.confirm(
1119-
f"Will generate an entrypoint at {destination}. Proceed?"
1155+
f"Will generate an entrypoint at {output}. Proceed?"
11201156
):
11211157
raise click.exceptions.Abort
11221158

1123-
destination.parent.mkdir(exist_ok=True)
1159+
output.parent.mkdir(exist_ok=True)
11241160

11251161
if (
11261162
not yes
1127-
and destination.exists()
1163+
and output.exists()
11281164
and not click.confirm(
1129-
f"Nextflow entrypoint already exists at `{destination}`. Overwrite?"
1165+
f"Nextflow entrypoint already exists at `{output}`. Overwrite?"
11301166
)
11311167
):
11321168
return
@@ -1151,11 +1187,7 @@ def generate_entrypoint(
11511187
raise click.exceptions.Exit(1)
11521188

11531189
generate_nextflow_workflow(
1154-
pkg_root,
1155-
metadata_root,
1156-
nf_script,
1157-
destination,
1158-
execution_profile=execution_profile,
1190+
pkg_root, metadata_root, nf_script, output, execution_profile=execution_profile
11591191
)
11601192

11611193

src/latch_cli/nextflow/parse_schema.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from dataclasses import dataclass
33
from pathlib import Path
44
from textwrap import indent
5-
from typing import Generic, Literal, Optional, TypedDict, TypeVar, Union
5+
from typing import Literal, Optional, TypedDict, Union
66

77
import click
88
from typing_extensions import NotRequired, TypeAlias
@@ -63,17 +63,33 @@ class NfBooleanType(TypedDict):
6363
metadata: CommonMetadata
6464

6565

66-
EnumT = TypeVar("EnumT", str, int, float)
66+
class NfStringEnumType(TypedDict):
67+
type: Literal["enum"]
68+
flavor: Literal["string"]
69+
values: list[str]
70+
default: NotRequired[str]
71+
metadata: CommonMetadata
72+
73+
74+
class NfIntegerEnumType(TypedDict):
75+
type: Literal["enum"]
76+
flavor: Literal["integer"]
77+
values: list[int]
78+
default: NotRequired[int]
79+
metadata: CommonMetadata
6780

6881

69-
class NfEnumType(TypedDict, Generic[EnumT]):
82+
class NfNumberEnumType(TypedDict):
7083
type: Literal["enum"]
71-
flavor: Literal["string", "integer", "number"]
72-
values: list[EnumT]
73-
default: NotRequired[EnumT]
84+
flavor: Literal["number"]
85+
values: list[float]
86+
default: NotRequired[float]
7487
metadata: CommonMetadata
7588

7689

90+
NfEnumType: TypeAlias = Union[NfStringEnumType, NfIntegerEnumType, NfNumberEnumType]
91+
92+
7793
class NfArrayType(TypedDict):
7894
type: Literal["array"]
7995
default: NotRequired[list]
@@ -106,9 +122,7 @@ class NfBlobType(TypedDict):
106122
NfStringType,
107123
NfIntegerType,
108124
NfFloatType,
109-
NfEnumType[str],
110-
NfEnumType[float],
111-
NfEnumType[int],
125+
NfEnumType,
112126
NfBooleanType,
113127
NfArrayType,
114128
NfObjectType,
@@ -261,7 +275,7 @@ def parse_bool(
261275

262276
def parse_enum(
263277
param_name: str, properties: dict[str, object], required_set: set[str]
264-
) -> Union[NfEnumType[str], NfEnumType[int], NfEnumType[float]]:
278+
) -> NfEnumType:
265279
typ = properties["type"]
266280
assert "enum" in properties
267281

@@ -277,13 +291,37 @@ def parse_enum(
277291
assert values is not None, "enum parameter must specify a set of `values`"
278292
assert isinstance(values, list), "`values` must be a list"
279293

280-
# todo(ayush): fix type errors here
281-
return NfEnumType(
294+
if typ == "string":
295+
assert default is None or isinstance(default, str), "default must be a string"
296+
297+
return NfStringEnumType(
298+
type="enum",
299+
flavor="string",
300+
values=values,
301+
metadata=metadata,
302+
**({"default": default} if default is not None else {}),
303+
)
304+
if typ == "integer":
305+
assert default is None or isinstance(default, int), "default must be an integer"
306+
307+
return NfIntegerEnumType(
308+
type="enum",
309+
flavor="integer",
310+
values=values,
311+
metadata=metadata,
312+
**({"default": default} if default is not None else {}),
313+
)
314+
315+
assert default is None or isinstance(default, (int, float)), (
316+
"default must be a number"
317+
)
318+
319+
return NfNumberEnumType(
282320
type="enum",
283-
flavor=typ,
321+
flavor="number",
284322
values=values,
285323
metadata=metadata,
286-
**({"default": default} if default is not None else {}),
324+
**({"default": float(default)} if default is not None else {}),
287325
)
288326

289327

src/latch_cli/services/init/init.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,9 @@ def init(
379379

380380
template_func(pkg_root)
381381

382-
config = get_or_create_workflow_config(pkg_root, base_image_type)
382+
config = get_or_create_workflow_config(
383+
pkg_root / ".latch" / "config", base_image_type
384+
)
383385

384386
wf_type = WorkflowType.latchbiosdk
385387
if chosen_template == "Snakemake Example":

src/latch_cli/workflow_config.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ class LatchWorkflowConfig:
3131

3232

3333
def get_or_create_workflow_config(
34-
pkg_root: Path, base_image_type: BaseImageOptions = BaseImageOptions.default
34+
config_path: Path, base_image_type: BaseImageOptions = BaseImageOptions.default
3535
) -> LatchWorkflowConfig:
36-
config_path = pkg_root / latch_constants.pkg_config
3736
if config_path.exists() and config_path.is_file():
3837
try:
3938
return LatchWorkflowConfig(**json.loads(config_path.read_text()))
@@ -60,9 +59,7 @@ def get_or_create_workflow_config(
6059
date=datetime.now(timezone.utc).isoformat(),
6160
)
6261

63-
(pkg_root / ".latch").mkdir(exist_ok=True)
64-
65-
with (pkg_root / latch_constants.pkg_config).open("w") as f:
66-
f.write(json.dumps(asdict(config)))
62+
config_path.parent.mkdir(parents=True, exist_ok=True)
63+
config_path.write_text(json.dumps(asdict(config)))
6764

6865
return config

0 commit comments

Comments
 (0)