lamin_cli 1.5.4__tar.gz → 1.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/PKG-INFO +1 -1
  2. lamin_cli-1.6.0/lamin_cli/__init__.py +3 -0
  3. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/lamin_cli/__main__.py +139 -54
  4. lamin_cli-1.6.0/lamin_cli/_annotate.py +47 -0
  5. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/lamin_cli/_save.py +26 -17
  6. lamin_cli-1.5.4/tests/core/test_save_files.py → lamin_cli-1.6.0/tests/core/test_save_annotate_files.py +42 -1
  7. lamin_cli-1.5.4/tests/core/test_save_scripts.py → lamin_cli-1.6.0/tests/core/test_save_annotate_scripts.py +11 -1
  8. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/test_save_notebooks.py +1 -1
  9. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/test_save_r_code.py +1 -2
  10. lamin_cli-1.5.4/lamin_cli/__init__.py +0 -3
  11. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/.github/workflows/build.yml +0 -0
  12. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/.github/workflows/doc-changes.yml +0 -0
  13. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/.gitignore +0 -0
  14. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/.pre-commit-config.yaml +0 -0
  15. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/LICENSE +0 -0
  16. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/README.md +0 -0
  17. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/lamin_cli/_cache.py +0 -0
  18. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/lamin_cli/_load.py +0 -0
  19. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/lamin_cli/_migration.py +0 -0
  20. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/lamin_cli/_settings.py +0 -0
  21. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/lamin_cli/compute/__init__.py +0 -0
  22. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/lamin_cli/compute/modal.py +0 -0
  23. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/noxfile.py +0 -0
  24. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/pyproject.toml +0 -0
  25. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/conftest.py +0 -0
  26. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/test_create_switch_delete_list.py +0 -0
  27. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/test_load.py +0 -0
  28. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/test_login.py +0 -0
  29. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/test_migrate.py +0 -0
  30. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/test_multi_process.py +0 -0
  31. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/core/test_parse_uid_from_code.py +0 -0
  32. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/modal/test_modal.py +0 -0
  33. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/notebooks/not-initialized.ipynb +0 -0
  34. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/notebooks/with-title-and-initialized-consecutive.ipynb +0 -0
  35. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +0 -0
  36. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/scripts/merely-import-lamindb.py +0 -0
  37. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/scripts/run-track-and-finish-sync-git.py +0 -0
  38. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/scripts/run-track-and-finish.py +0 -0
  39. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/scripts/run-track-with-params.py +0 -0
  40. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/scripts/run-track.R +0 -0
  41. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/scripts/run-track.qmd +0 -0
  42. {lamin_cli-1.5.4 → lamin_cli-1.6.0}/tests/scripts/testscript.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lamin_cli
3
- Version: 1.5.4
3
+ Version: 1.6.0
4
4
  Summary: Lamin CLI.
5
5
  Author-email: Lamin Labs <open-source@lamin.ai>
6
6
  Description-Content-Type: text/markdown
@@ -0,0 +1,3 @@
1
+ """Lamin CLI."""
2
+
3
+ __version__ = "1.6.0"
@@ -22,6 +22,34 @@ from lamindb_setup._init_instance import (
22
22
  if TYPE_CHECKING:
23
23
  from collections.abc import Mapping
24
24
 
25
+ COMMAND_GROUPS = {
26
+ "lamin": [
27
+ {
28
+ "name": "Manage connections",
29
+ "commands": ["connect", "info", "init", "disconnect"],
30
+ },
31
+ {
32
+ "name": "Load, save, create & delete data",
33
+ "commands": ["load", "save", "create", "delete"],
34
+ },
35
+ {
36
+ "name": "Describe, annotate & list data",
37
+ "commands": ["describe", "annotate", "list"],
38
+ },
39
+ {
40
+ "name": "Configure",
41
+ "commands": ["checkout", "switch", "cache", "settings", "migrate"],
42
+ },
43
+ {
44
+ "name": "Auth",
45
+ "commands": [
46
+ "login",
47
+ "logout",
48
+ ],
49
+ },
50
+ ]
51
+ }
52
+
25
53
  # https://github.com/ewels/rich-click/issues/19
26
54
  # Otherwise rich-click takes over the formatting.
27
55
  if os.environ.get("NO_RICH"):
@@ -47,30 +75,6 @@ if os.environ.get("NO_RICH"):
47
75
  else:
48
76
  import rich_click as click
49
77
 
50
- COMMAND_GROUPS = {
51
- "lamin": [
52
- {
53
- "name": "Connect to an instance",
54
- "commands": ["connect", "disconnect", "info", "init", "run"],
55
- },
56
- {
57
- "name": "Read & write data",
58
- "commands": ["load", "save", "get", "delete", "create", "list"],
59
- },
60
- {
61
- "name": "Configure",
62
- "commands": ["checkout", "switch", "cache", "settings", "migrate"],
63
- },
64
- {
65
- "name": "Auth",
66
- "commands": [
67
- "login",
68
- "logout",
69
- ],
70
- },
71
- ]
72
- }
73
-
74
78
  def lamin_group_decorator(f):
75
79
  @click.rich_config(
76
80
  help_config=click.RichHelpConfiguration(
@@ -104,7 +108,7 @@ except PackageNotFoundError:
104
108
  @lamin_group_decorator
105
109
  @click.version_option(version=lamindb_version, prog_name="lamindb")
106
110
  def main():
107
- """Configure LaminDB and perform simple actions."""
111
+ """Manage data with LaminDB instances."""
108
112
  silence_loggers()
109
113
 
110
114
 
@@ -190,13 +194,10 @@ def connect(instance: str):
190
194
  {attr}`~lamindb.setup.core.SetupSettings.auto_connect` to `True` so that you
191
195
  auto-connect in a Python session upon importing `lamindb`.
192
196
 
193
- For manually connecting in a Python session, use {func}`~lamindb.connect`.
197
+ Alternatively, you can connect in a Python session via {func}`~lamindb.connect`.
194
198
  """
195
- from lamindb_setup import connect as connect_
196
- from lamindb_setup import settings as settings_
197
-
198
- settings_.auto_connect = True
199
- return connect_(instance, _reload_lamindb=False, _write_settings=True)
199
+ from lamindb_setup._connect_instance import _connect_cli
200
+ return _connect_cli(instance)
200
201
 
201
202
 
202
203
  @main.command()
@@ -263,7 +264,10 @@ def list_(entity: Literal["branch"], name: str | None = None):
263
264
  def switch(branch: str | None = None, space: str | None = None):
264
265
  """Switch between branches or spaces.
265
266
 
266
- Currently only supports creating a branch.
267
+ ```
268
+ lamin switch --branch my_branch
269
+ lamin switch --space our_space
270
+ ```
267
271
  """
268
272
  from lamindb.setup import switch as switch_
269
273
 
@@ -273,7 +277,7 @@ def switch(branch: str | None = None, space: str | None = None):
273
277
  @main.command()
274
278
  @click.option("--schema", is_flag=True, help="View database schema.")
275
279
  def info(schema: bool):
276
- """Show info about current instance."""
280
+ """Show info about the environment, instance, branch, space, and user."""
277
281
  if schema:
278
282
  from lamindb_setup._schema import view
279
283
 
@@ -289,13 +293,14 @@ def info(schema: bool):
289
293
  @main.command()
290
294
  @click.argument("entity", type=str)
291
295
  @click.option("--name", type=str, default=None)
296
+ @click.option("--uid", type=str, default=None)
292
297
  @click.option("--slug", type=str, default=None)
293
298
  @click.option("--force", is_flag=True, default=False, help="Do not ask for confirmation (only relevant for instance).")
294
299
  # fmt: on
295
- def delete(entity: str, name: str | None = None, slug: str | None = None, force: bool = False):
300
+ def delete(entity: str, name: str | None = None, uid: str | None = None, slug: str | None = None, force: bool = False):
296
301
  """Delete an entity.
297
302
 
298
- Currently supported: `branch` and `instance`.
303
+ Currently supported: `branch`, `artifact`, and `instance`.
299
304
 
300
305
  ```
301
306
  lamin delete instance --slug account/name
@@ -305,9 +310,15 @@ def delete(entity: str, name: str | None = None, slug: str | None = None, force:
305
310
  from lamindb_setup._delete import delete
306
311
 
307
312
  if entity == "branch":
313
+ assert name is not None, "You have to pass a name for deleting a branch."
308
314
  from lamindb import Branch
309
315
 
310
316
  Branch.get(name=name).delete()
317
+ elif entity == "artifact":
318
+ assert uid is not None, "You have to pass a uid for deleting an artifact."
319
+ from lamindb import Artifact
320
+
321
+ Artifact.get(uid).delete()
311
322
  elif entity == "instance":
312
323
  return delete(slug, force=force)
313
324
  else: # backwars compatibility
@@ -322,7 +333,7 @@ def delete(entity: str, name: str | None = None, slug: str | None = None, force:
322
333
  "--with-env", is_flag=True, help="Also return the environment for a tranform."
323
334
  )
324
335
  def load(entity: str, uid: str | None = None, key: str | None = None, with_env: bool = False):
325
- """Load a file or folder.
336
+ """Load a file or folder into the cache or working directory.
326
337
 
327
338
  Pass a URL, `artifact`, or `transform`. For example:
328
339
 
@@ -337,30 +348,16 @@ def load(entity: str, uid: str | None = None, key: str | None = None, with_env:
337
348
  """
338
349
  is_slug = entity.count("/") == 1
339
350
  if is_slug:
340
- from lamindb_setup import connect
341
- from lamindb_setup import settings as settings_
342
-
343
- # can decide whether we want to actually deprecate
344
- # click.echo(
345
- # f"! please use: lamin connect {entity}"
346
- # )
347
- settings_.auto_connect = True
348
- return connect(entity, _reload_lamindb=False, _write_settings=True)
351
+ from lamindb_setup._connect_instance import _connect_cli
352
+ # for backward compat and convenience, connect to the instance
353
+ return _connect_cli(entity)
349
354
  else:
350
355
  from lamin_cli._load import load as load_
351
356
 
352
357
  return load_(entity, uid=uid, key=key, with_env=with_env)
353
358
 
354
359
 
355
- @main.command()
356
- @click.argument("entity", type=str)
357
- @click.option("--uid", help="The uid for the entity.")
358
- @click.option("--key", help="The key for the entity.")
359
- def get(entity: str, uid: str | None = None, key: str | None = None):
360
- """Query metadata about an entity.
361
-
362
- Currently only works for artifact.
363
- """
360
+ def _describe(entity: str = "artifact", uid: str | None = None, key: str | None = None):
364
361
  import lamindb_setup as ln_setup
365
362
 
366
363
  from ._load import decompose_url
@@ -383,6 +380,34 @@ def get(entity: str, uid: str | None = None, key: str | None = None):
383
380
  artifact.describe()
384
381
 
385
382
 
383
+ @main.command()
384
+ @click.argument("entity", type=str, default="artifact")
385
+ @click.option("--uid", help="The uid for the entity.")
386
+ @click.option("--key", help="The key for the entity.")
387
+ def describe(entity: str = "artifact", uid: str | None = None, key: str | None = None):
388
+ """Describe an artifact.
389
+
390
+ ```
391
+ lamin describe --key example_datasets/mini_immuno/dataset1.h5ad
392
+ lamin describe https://lamin.ai/laminlabs/lamin-site-assets/artifact/6sofuDVvTANB0f48
393
+ ```
394
+ """
395
+ _describe(entity=entity, uid=uid, key=key)
396
+
397
+
398
+ @main.command()
399
+ @click.argument("entity", type=str, default="artifact")
400
+ @click.option("--uid", help="The uid for the entity.")
401
+ @click.option("--key", help="The key for the entity.")
402
+ def get(entity: str = "artifact", uid: str | None = None, key: str | None = None):
403
+ """Query metadata about an entity.
404
+
405
+ Currently still equivalent to `lamin describe`.
406
+ """
407
+ logger.warning("please use `lamin describe` instead of `lamin get` to describe")
408
+ _describe(entity=entity, uid=uid, key=key)
409
+
410
+
386
411
  @main.command()
387
412
  @click.argument("path", type=str)
388
413
  @click.option("--key", type=str, default=None, help="The key of the artifact or transform.")
@@ -414,6 +439,66 @@ def save(path: str, key: str, description: str, stem_uid: str, project: str, spa
414
439
  sys.exit(1)
415
440
 
416
441
 
442
+ @main.command()
443
+ @click.option("--key", type=str, default=None, help="The key of an artifact or transform.")
444
+ @click.option("--uid", type=str, default=None, help="The uid of an artifact or transform.")
445
+ @click.option("--project", type=str, default=None, help="A valid project name or uid.")
446
+ @click.option("--features", multiple=True, help="Feature annotations. Supports: feature=value, feature=val1,val2, or feature=\"val1\",\"val2\"")
447
+ @click.option("--registry", type=str, default=None, help="Either 'artifact' or 'transform'. If not passed, chooses based on key suffix.")
448
+ def annotate(key: str, uid: str, project: str, registry: str, features: tuple):
449
+ """Annotate an artifact or a transform.
450
+
451
+ You can annotate with projects and valid features & values.
452
+
453
+ ```
454
+ lamin annotate --key raw/sample.fastq --project "My Project"
455
+ lamin annotate --key raw/sample.fastq --features perturbation=IFNG,DMSO cell_line=HEK297
456
+ lamin annotate --key my-notebook.ipynb --project "My Project"
457
+ ```
458
+ """
459
+ import lamindb as ln
460
+
461
+ from lamin_cli._annotate import _parse_features_list
462
+ from lamin_cli._save import infer_registry_from_path
463
+
464
+ if registry is None:
465
+ if key is not None:
466
+ registry = infer_registry_from_path(key)
467
+ else:
468
+ registry = "artifact"
469
+ if registry == "artifact":
470
+ model = ln.Artifact
471
+ else:
472
+ model = ln.Transform
473
+
474
+ # Get the artifact
475
+ if key is not None:
476
+ artifact = model.get(key=key)
477
+ elif uid is not None:
478
+ artifact = model.get(uid=uid)
479
+ else:
480
+ raise ln.errors.InvalidArgument("Either --key or --uid must be provided")
481
+
482
+ # Handle project annotation
483
+ if project is not None:
484
+ project_record = ln.Project.filter(
485
+ ln.Q(name=project) | ln.Q(uid=project)
486
+ ).one_or_none()
487
+ if project_record is None:
488
+ raise ln.errors.InvalidArgument(
489
+ f"Project '{project}' not found, either create it with `ln.Project(name='...').save()` or fix typos."
490
+ )
491
+ artifact.projects.add(project_record)
492
+
493
+ # Handle feature annotations
494
+ if features:
495
+ feature_dict = _parse_features_list(features)
496
+ artifact.features.add_values(feature_dict)
497
+
498
+ artifact_rep = artifact.key if artifact.key else artifact.description if artifact.description else artifact.uid
499
+ logger.important(f"annotated {registry}: {artifact_rep}")
500
+
501
+
417
502
  @main.command()
418
503
  @click.argument("filepath", type=str)
419
504
  @click.option("--project", type=str, default=None, help="A valid project name or uid. When running on Modal, creates an app with the same name.", required=True)
@@ -0,0 +1,47 @@
1
+ def _parse_features_list(features_list: tuple) -> dict:
2
+ """Parse feature list into a dictionary.
3
+
4
+ Supports multiple formats:
5
+ - Quoted values: 'perturbation="DMSO","IFNG"' → {"perturbation": ["DMSO", "IFNG"]}
6
+ - Unquoted values: 'perturbation=IFNG,DMSO' → {"perturbation": ["IFNG", "DMSO"]}
7
+ - Single values: 'cell_line=HEK297' → {"cell_line": "HEK297"}
8
+ - Mixed: ('perturbation="DMSO","IFNG"', 'cell_line=HEK297', 'genes=TP53,BRCA1')
9
+ """
10
+ import re
11
+
12
+ import lamindb as ln
13
+
14
+ feature_dict = {}
15
+
16
+ for feature_assignment in features_list:
17
+ if "=" not in feature_assignment:
18
+ raise ln.errors.InvalidArgument(
19
+ f"Invalid feature assignment: '{feature_assignment}'. Expected format: 'feature=value' or 'feature=\"value1\",\"value2\"'"
20
+ )
21
+
22
+ feature_name, values_str = feature_assignment.split("=", 1)
23
+ feature_name = feature_name.strip()
24
+
25
+ # Parse quoted values using regex
26
+ # This will match quoted strings like "DMSO","IFNG" or single values like HEK297
27
+ quoted_values = re.findall(r'"([^"]*)"', values_str)
28
+
29
+ if quoted_values:
30
+ # If we found quoted values, use them
31
+ if len(quoted_values) == 1:
32
+ feature_dict[feature_name] = quoted_values[0]
33
+ else:
34
+ feature_dict[feature_name] = quoted_values
35
+ else:
36
+ # If no quoted values, treat as single unquoted value
37
+ # Remove any surrounding whitespace
38
+ value = values_str.strip()
39
+
40
+ # Handle comma-separated unquoted values
41
+ if "," in value:
42
+ values = [v.strip() for v in value.split(",")]
43
+ feature_dict[feature_name] = values
44
+ else:
45
+ feature_dict[feature_name] = value
46
+
47
+ return feature_dict
@@ -1,15 +1,29 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import re
4
5
  import sys
6
+ from pathlib import Path
5
7
  from typing import TYPE_CHECKING
6
8
 
7
9
  import click
8
10
  from lamin_utils import logger
9
11
  from lamindb_setup.core.hashing import hash_file
10
12
 
11
- if TYPE_CHECKING:
12
- from pathlib import Path
13
+
14
+ def infer_registry_from_path(path: Path | str) -> str:
15
+ suffixes_transform = {
16
+ "py": {".py", ".ipynb"},
17
+ "R": {".R", ".qmd", ".Rmd"},
18
+ }
19
+ if isinstance(path, str):
20
+ path = Path(path)
21
+ registry = (
22
+ "transform"
23
+ if path.suffix in suffixes_transform["py"].union(suffixes_transform["R"])
24
+ else "artifact"
25
+ )
26
+ return registry
13
27
 
14
28
 
15
29
  def parse_uid_from_code(content: str, suffix: str) -> str | None:
@@ -82,15 +96,7 @@ def save_from_path_cli(
82
96
  raise click.BadParameter(f"Path {path} does not exist", param_hint="path")
83
97
 
84
98
  if registry is None:
85
- suffixes_transform = {
86
- "py": {".py", ".ipynb"},
87
- "R": {".R", ".qmd", ".Rmd"},
88
- }
89
- registry = (
90
- "transform"
91
- if path.suffix in suffixes_transform["py"].union(suffixes_transform["R"])
92
- else "artifact"
93
- )
99
+ registry = infer_registry_from_path(path)
94
100
 
95
101
  if project is not None:
96
102
  project_record = ln.Project.filter(
@@ -145,7 +151,7 @@ def save_from_path_cli(
145
151
  artifact.save()
146
152
  logger.important(f"saved: {artifact}")
147
153
  logger.important(f"storage path: {artifact.path}")
148
- if ln_setup.settings.storage.type == "s3":
154
+ if artifact.storage.type == "s3":
149
155
  logger.important(f"storage url: {artifact.path.to_url()}")
150
156
  if project is not None:
151
157
  artifact.projects.add(project_record)
@@ -256,11 +262,14 @@ def save_from_path_cli(
256
262
  # latest run of this transform by user
257
263
  run = ln.Run.filter(transform=transform).order_by("-started_at").first()
258
264
  if run is not None and run.created_by.id != ln_setup.settings.user.id:
259
- response = input(
260
- "You are trying to save a transform created by another user: Source"
261
- " and report files will be tagged with *your* user id. Proceed?"
262
- " (y/n)"
263
- )
265
+ if os.environ.get("LAMIN_TESTING") == "true":
266
+ response = "y"
267
+ else:
268
+ response = input(
269
+ "You are trying to save a transform created by another user: Source"
270
+ " and report files will be tagged with *your* user id. Proceed?"
271
+ " (y/n)"
272
+ )
264
273
  if response != "y":
265
274
  return "aborted-save-notebook-created-by-different-user"
266
275
  if run is None and transform.key.endswith(".ipynb"):
@@ -1,3 +1,4 @@
1
+ import re
1
2
  import subprocess
2
3
  from pathlib import Path
3
4
 
@@ -7,7 +8,7 @@ import lamindb_setup as ln_setup
7
8
  test_file = Path(__file__).parent.parent.parent.resolve() / ".gitignore"
8
9
 
9
10
 
10
- def test_save_local_file():
11
+ def test_save_and_annotate_local_file():
11
12
  filepath = test_file
12
13
 
13
14
  # neither key nor description
@@ -71,6 +72,46 @@ def test_save_local_file():
71
72
  )
72
73
  assert result.returncode == 1
73
74
 
75
+ result = subprocess.run(
76
+ f"lamin save {filepath} --key mytest --registry artifact",
77
+ shell=True,
78
+ capture_output=True,
79
+ )
80
+ print(result.stdout.decode())
81
+ print(result.stderr.decode())
82
+ assert "returning existing artifact with same hash" in result.stdout.decode()
83
+ assert "key='mytest'" in result.stdout.decode()
84
+ assert "storage path:" in result.stdout.decode()
85
+ assert result.returncode == 0
86
+
87
+ artifact.projects.remove(project)
88
+
89
+ ml_split_type = ln.ULabel(name="Perturbation", is_type=True).save()
90
+ ln.ULabel(name="DMSO", type=ml_split_type).save()
91
+ ln.ULabel(name="IFNG", type=ml_split_type).save()
92
+ ln.Feature(name="perturbation", dtype=ml_split_type).save()
93
+
94
+ result = subprocess.run(
95
+ "lamin annotate --key mytest --project test_project --features perturbation=DMSO,IFNG",
96
+ shell=True,
97
+ capture_output=True,
98
+ )
99
+ print(result.stdout.decode())
100
+ print(result.stderr.decode())
101
+ assert result.returncode == 0
102
+
103
+ artifact = ln.Artifact.get(key="mytest")
104
+ features = artifact.features.get_values()
105
+ assert features["perturbation"] == {"DMSO", "IFNG"}
106
+ assert project in artifact.projects.all()
107
+
108
+ result = subprocess.run(
109
+ "lamin describe --key mytest",
110
+ shell=True,
111
+ capture_output=True,
112
+ )
113
+ assert result.returncode == 0
114
+
74
115
 
75
116
  def test_save_cloud_file():
76
117
  # should be no key for cloud paths
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  import subprocess
3
4
  from pathlib import Path
4
5
 
@@ -29,7 +30,7 @@ def test_save_resave_script_no_uids():
29
30
  assert ln.Transform.filter(key=filepath.name).count() == 2
30
31
 
31
32
 
32
- def test_save_without_uid():
33
+ def test_save_and_annotate_without_uid():
33
34
  env = os.environ
34
35
  env["LAMIN_TESTING"] = "true"
35
36
  filepath = scripts_dir / "run-track-and-finish.py"
@@ -47,6 +48,15 @@ def test_save_without_uid():
47
48
  assert "created Transform" in result.stdout.decode()
48
49
  assert "labeled with project: test_project" in result.stdout.decode()
49
50
 
51
+ result = subprocess.run(
52
+ "lamin annotate --key run-track-and-finish.py --project test_project",
53
+ shell=True,
54
+ capture_output=True,
55
+ )
56
+ print(result.stdout.decode())
57
+ print(result.stderr.decode())
58
+ assert result.returncode == 0
59
+
50
60
 
51
61
  def test_run_save_cache_with_git_and_uid():
52
62
  env = os.environ
@@ -35,7 +35,7 @@ def test_save_non_consecutive():
35
35
  transform = ln.Transform(
36
36
  uid="HDMGkxN9rgFA0000",
37
37
  version="1",
38
- name="My test notebook (non-consecutive)",
38
+ key="My test notebook (non-consecutive)",
39
39
  type="notebook",
40
40
  ).save()
41
41
  ln.Run(transform=transform).save()
@@ -13,7 +13,7 @@ def test_run_save_cache():
13
13
  filepath = scripts_dir / "run-track.R"
14
14
 
15
15
  transform = ln.Transform(
16
- uid="EPnfDtJz8qbE0000", name="run-track.R", key="run-track.R", type="script"
16
+ uid="EPnfDtJz8qbE0000", key="run-track.R", type="script"
17
17
  ).save()
18
18
  ln.Run(transform=transform).save()
19
19
 
@@ -38,7 +38,6 @@ def test_run_save_cache():
38
38
 
39
39
  transform = ln.Transform(
40
40
  uid="HPnfDtJz8qbE0000",
41
- name="run-track.qmd",
42
41
  key="run-track.qmd",
43
42
  type="notebook",
44
43
  ).save()
@@ -1,3 +0,0 @@
1
- """Lamin CLI."""
2
-
3
- __version__ = "1.5.4"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes