lamin_cli 1.5.5__py2.py3-none-any.whl → 1.6.0__py2.py3-none-any.whl

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.
lamin_cli/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Lamin CLI."""
2
2
 
3
- __version__ = "1.5.5"
3
+ __version__ = "1.6.0"
lamin_cli/__main__.py CHANGED
@@ -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
 
@@ -329,7 +333,7 @@ def delete(entity: str, name: str | None = None, uid: str | None = None, slug: s
329
333
  "--with-env", is_flag=True, help="Also return the environment for a tranform."
330
334
  )
331
335
  def load(entity: str, uid: str | None = None, key: str | None = None, with_env: bool = False):
332
- """Load a file or folder.
336
+ """Load a file or folder into the cache or working directory.
333
337
 
334
338
  Pass a URL, `artifact`, or `transform`. For example:
335
339
 
@@ -344,30 +348,16 @@ def load(entity: str, uid: str | None = None, key: str | None = None, with_env:
344
348
  """
345
349
  is_slug = entity.count("/") == 1
346
350
  if is_slug:
347
- from lamindb_setup import connect
348
- from lamindb_setup import settings as settings_
349
-
350
- # can decide whether we want to actually deprecate
351
- # click.echo(
352
- # f"! please use: lamin connect {entity}"
353
- # )
354
- settings_.auto_connect = True
355
- 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)
356
354
  else:
357
355
  from lamin_cli._load import load as load_
358
356
 
359
357
  return load_(entity, uid=uid, key=key, with_env=with_env)
360
358
 
361
359
 
362
- @main.command()
363
- @click.argument("entity", type=str)
364
- @click.option("--uid", help="The uid for the entity.")
365
- @click.option("--key", help="The key for the entity.")
366
- def get(entity: str, uid: str | None = None, key: str | None = None):
367
- """Query metadata about an entity.
368
-
369
- Currently only works for artifact.
370
- """
360
+ def _describe(entity: str = "artifact", uid: str | None = None, key: str | None = None):
371
361
  import lamindb_setup as ln_setup
372
362
 
373
363
  from ._load import decompose_url
@@ -390,6 +380,34 @@ def get(entity: str, uid: str | None = None, key: str | None = None):
390
380
  artifact.describe()
391
381
 
392
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
+
393
411
  @main.command()
394
412
  @click.argument("path", type=str)
395
413
  @click.option("--key", type=str, default=None, help="The key of the artifact or transform.")
@@ -421,6 +439,66 @@ def save(path: str, key: str, description: str, stem_uid: str, project: str, spa
421
439
  sys.exit(1)
422
440
 
423
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
+
424
502
  @main.command()
425
503
  @click.argument("filepath", type=str)
426
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)
lamin_cli/_annotate.py ADDED
@@ -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
lamin_cli/_save.py CHANGED
@@ -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(
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lamin_cli
3
- Version: 1.5.5
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,15 @@
1
+ lamin_cli/__init__.py,sha256=RMGZmTcFDyOsY_o5u0yTfy5XRyzFXCN8Ylh6drfn6iU,40
2
+ lamin_cli/__main__.py,sha256=CeFFK696hYVY19dNHbUeZ8-OVByVFsNiVybkDnD_Ras,18936
3
+ lamin_cli/_annotate.py,sha256=ZD76__K-mQt7UpYqyM1I2lKCs-DraTmnkjsByIHmD-g,1839
4
+ lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
5
+ lamin_cli/_load.py,sha256=QqiFxGYmvFz2RjhvwKO5r1fmPWMZ5Ai4y1pJytdYKak,8301
6
+ lamin_cli/_migration.py,sha256=xQi6mwnpBzY5wcv1-TJhveD7a3XJIlpiYx6Z3AJ1NF0,1063
7
+ lamin_cli/_save.py,sha256=fC_tD65x3wufXjPyN36Xhl5gSuBj8FsD3G1JRNHmJrw,11021
8
+ lamin_cli/_settings.py,sha256=O2tecCf5EIZu98ima4DTJujo4KuywckOLgw8c-Ke3dY,1142
9
+ lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
11
+ lamin_cli-1.6.0.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
12
+ lamin_cli-1.6.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
13
+ lamin_cli-1.6.0.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
14
+ lamin_cli-1.6.0.dist-info/METADATA,sha256=jePbx3ZZwBQIyU_IsSOQk1e3DoVzHBG4VaPEQTDTGz8,337
15
+ lamin_cli-1.6.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- lamin_cli/__init__.py,sha256=TjqhH_7HYqV7eEmqmrHxIzQ-nXehrr75pFeHCnGQCxY,40
2
- lamin_cli/__main__.py,sha256=BO_vUmKhOF3WHBXze70rmHt-obCsgRhNYtT_-rGuH4Y,15886
3
- lamin_cli/_cache.py,sha256=oplwE8AcS_9PYptQUZxff2qTIdNFS81clGPkJNWk098,800
4
- lamin_cli/_load.py,sha256=QqiFxGYmvFz2RjhvwKO5r1fmPWMZ5Ai4y1pJytdYKak,8301
5
- lamin_cli/_migration.py,sha256=xQi6mwnpBzY5wcv1-TJhveD7a3XJIlpiYx6Z3AJ1NF0,1063
6
- lamin_cli/_save.py,sha256=AdghV9arOEANqkT-5hrhBEanoVrOlQKhejlzPJLoUes,10760
7
- lamin_cli/_settings.py,sha256=O2tecCf5EIZu98ima4DTJujo4KuywckOLgw8c-Ke3dY,1142
8
- lamin_cli/compute/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- lamin_cli/compute/modal.py,sha256=QnR7GyyvWWWkLnou95HxS9xxSQfw1k-SiefM_qRVnU0,6010
10
- lamin_cli-1.5.5.dist-info/entry_points.txt,sha256=Qms85i9cZPlu-U7RnVZhFsF7vJ9gaLZUFkCjcGcXTpg,49
11
- lamin_cli-1.5.5.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
12
- lamin_cli-1.5.5.dist-info/WHEEL,sha256=ssQ84EZ5gH1pCOujd3iW7HClo_O_aDaClUbX4B8bjKY,100
13
- lamin_cli-1.5.5.dist-info/METADATA,sha256=3CxozXOIRc1FFUzD1letgB9-nXa92fbLN5qmnpEg0n4,337
14
- lamin_cli-1.5.5.dist-info/RECORD,,