lamin_cli 1.10.0__py2.py3-none-any.whl → 1.12.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
@@ -6,7 +6,7 @@ The interface is defined in `__main__.py`.
6
6
  The root API here is used by LaminR to replicate the CLI functionality.
7
7
  """
8
8
 
9
- __version__ = "1.10.0"
9
+ __version__ = "1.12.0"
10
10
 
11
11
  from lamindb_setup import disconnect, logout
12
12
  from lamindb_setup._connect_instance import _connect_cli as connect
lamin_cli/__main__.py CHANGED
@@ -42,6 +42,10 @@ COMMAND_GROUPS = {
42
42
  "name": "Load, save, create & delete data",
43
43
  "commands": ["load", "save", "create", "delete"],
44
44
  },
45
+ {
46
+ "name": "Tracking within shell scripts",
47
+ "commands": ["track", "finish"],
48
+ },
45
49
  {
46
50
  "name": "Describe, annotate & list data",
47
51
  "commands": ["describe", "annotate", "list"],
@@ -49,9 +53,7 @@ COMMAND_GROUPS = {
49
53
  {
50
54
  "name": "Configure",
51
55
  "commands": [
52
- "checkout",
53
56
  "switch",
54
- "cache",
55
57
  "settings",
56
58
  "migrate",
57
59
  ],
@@ -108,7 +110,6 @@ else:
108
110
 
109
111
  from lamindb_setup._silence_loggers import silence_loggers
110
112
 
111
- from lamin_cli._cache import cache
112
113
  from lamin_cli._io import io
113
114
  from lamin_cli._migration import migrate
114
115
  from lamin_cli._settings import settings
@@ -142,7 +143,7 @@ def login(user: str, key: str | None):
142
143
 
143
144
  After authenticating once, you can re-authenticate and switch between accounts via `lamin login myhandle`.
144
145
 
145
- See also: Login in a Python session via {func}`~lamindb.setup.login`.
146
+ Python/R alternative: {func}`~lamindb.setup.login`
146
147
  """
147
148
  return login_(user, key=key)
148
149
 
@@ -177,9 +178,19 @@ def init(
177
178
  db: str | None,
178
179
  modules: str | None,
179
180
  ):
180
- """Init a LaminDB instance.
181
+ """Initialize an instance.
182
+
183
+ This initializes a LaminDB instance, for example:
184
+
185
+ ```
186
+ lamin init --storage ./mydata
187
+ lamin init --storage s3://my-bucket
188
+ lamin init --storage gs://my-bucket
189
+ lamin init --storage ./mydata --modules bionty
190
+ lamin init --storage ./mydata --modules bionty,pertdb
191
+ ```
181
192
 
182
- See also: Init in a Python session via {func}`~lamindb.setup.init`.
193
+ Python/R alternative: {func}`~lamindb.setup.init`
183
194
  """
184
195
  return init_(storage=storage, db=db, modules=modules, name=name)
185
196
 
@@ -187,73 +198,108 @@ def init(
187
198
  # fmt: off
188
199
  @main.command()
189
200
  @click.argument("instance", type=str)
190
- @click.option("--use_proxy_db", is_flag=True, help="Use proxy database connection.")
191
201
  # fmt: on
192
- def connect(instance: str, use_proxy_db: bool):
193
- """Configure default instance for connections.
202
+ def connect(instance: str):
203
+ """Set a default instance for auto-connection.
194
204
 
195
- Python/R sessions and CLI commands will then auto-connect to the configured instance.
205
+ Python/R sessions and CLI commands will then auto-connect to this LaminDB instance.
196
206
 
197
- Pass a slug (`account/name`) or URL (`https://lamin.ai/account/name`).
207
+ Pass a slug (`account/name`) or URL (`https://lamin.ai/account/name`), for example:
198
208
 
199
- See also: Connect in a Python session via {func}`~lamindb.connect`.
209
+ ```
210
+ lamin connect laminlabs/cellxgene
211
+ lamin connect https://lamin.ai/laminlabs/cellxgene
212
+ ```
213
+
214
+ → Python/R alternative: {func}`~lamindb.connect` the global default database or a database object via {class}`~lamindb.DB`
200
215
  """
201
- return connect_(instance, use_proxy_db=use_proxy_db)
216
+ return connect_(instance)
202
217
 
203
218
 
204
219
  @main.command()
205
220
  def disconnect():
206
- """Clear default instance configuration.
221
+ """Unset the default instance for auto-connection.
222
+
223
+ Python/R sessions and CLI commands will no longer auto-connect to a LaminDB instance.
207
224
 
208
- See also: Disconnect in a Python session via {func}`~lamindb.setup.disconnect`.
225
+ For example:
226
+
227
+ ```
228
+ lamin disconnect
229
+ ```
230
+
231
+ → Python/R alternative: {func}`~lamindb.setup.disconnect`
209
232
  """
210
233
  return disconnect_()
211
234
 
212
235
 
213
236
  # fmt: off
214
237
  @main.command()
215
- @click.argument("entity", type=str)
216
- @click.option("--name", type=str, default=None, help="A name.")
238
+ @click.argument("registry", type=click.Choice(["branch", "project"]))
239
+ @click.argument("name", type=str, required=False)
240
+ # below is deprecated, for backward compatibility
241
+ @click.option("--name", "name_opt", type=str, default=None, hidden=True, help="A name.")
217
242
  # fmt: on
218
- def create(entity: Literal["branch"], name: str | None = None):
219
- """Create a record for an entity.
243
+ def create(
244
+ registry: Literal["branch", "project"],
245
+ name: str | None,
246
+ name_opt: str | None,
247
+ ):
248
+ """Create an object.
220
249
 
221
250
  Currently only supports creating branches and projects.
222
251
 
223
252
  ```
224
- lamin create branch --name my_branch
225
- lamin create project --name my_project
253
+ lamin create branch my_branch
254
+ lamin create project my_project
226
255
  ```
256
+
257
+ → Python/R alternative: {class}`~lamindb.Branch` and {class}`~lamindb.Project`.
227
258
  """
259
+ resolved_name = name if name is not None else name_opt
260
+ if resolved_name is None:
261
+ raise click.UsageError(
262
+ "Specify a name. Examples: lamin create branch my_branch, lamin create project my_project"
263
+ )
264
+ if name_opt is not None:
265
+ warnings.warn(
266
+ "lamin create --name is deprecated; use 'lamin create <registry> <name>' instead, e.g. lamin create branch my_branch.",
267
+ DeprecationWarning,
268
+ stacklevel=2,
269
+ )
270
+
228
271
  from lamindb.models import Branch, Project
229
272
 
230
- if entity == "branch":
231
- record = Branch(name=name).save()
232
- elif entity == "project":
233
- record = Project(name=name).save()
273
+ if registry == "branch":
274
+ record = Branch(name=resolved_name).save()
275
+ elif registry == "project":
276
+ record = Project(name=resolved_name).save()
234
277
  else:
235
- raise NotImplementedError(f"Creating {entity} is not implemented.")
236
- logger.important(f"created {entity}: {record.name}")
278
+ raise NotImplementedError(f"Creating {registry} object is not implemented.")
279
+ logger.important(f"created {registry}: {record.name}")
237
280
 
238
281
 
239
282
  # fmt: off
240
283
  @main.command(name="list")
241
- @click.argument("entity", type=str)
242
- @click.option("--name", type=str, default=None, help="A name.")
284
+ @click.argument("registry", type=str)
243
285
  # fmt: on
244
- def list_(entity: Literal["branch"], name: str | None = None):
245
- """List records for an entity.
286
+ def list_(registry: Literal["branch", "space"]):
287
+ """List objects.
288
+
289
+ For example:
246
290
 
247
291
  ```
248
292
  lamin list branch
249
293
  lamin list space
250
294
  ```
295
+
296
+ → Python/R alternative: {method}`~lamindb.Branch.to_dataframe()`
251
297
  """
252
- assert entity in {"branch", "space"}, "Currently only supports listing branches and spaces."
298
+ assert registry in {"branch", "space"}, "Currently only supports listing branches and spaces."
253
299
 
254
300
  from lamindb.models import Branch, Space
255
301
 
256
- if entity == "branch":
302
+ if registry == "branch":
257
303
  print(Branch.to_dataframe())
258
304
  else:
259
305
  print(Space.to_dataframe())
@@ -261,28 +307,56 @@ def list_(entity: Literal["branch"], name: str | None = None):
261
307
 
262
308
  # fmt: off
263
309
  @main.command()
264
- @click.option("--branch", type=str, default=None, help="A valid branch name or uid.")
265
- @click.option("--space", type=str, default=None, help="A valid branch name or uid.")
310
+ @click.argument("registry", type=click.Choice(["branch", "space"]), required=False)
311
+ @click.argument("name", type=str, required=False)
312
+ # below are deprecated, for backward compatibility
313
+ @click.option("--branch", type=str, default=None, hidden=True, help="A valid branch name or uid.")
314
+ @click.option("--space", type=str, default=None, hidden=True, help="A valid space name or uid.")
266
315
  # fmt: on
267
- def switch(branch: str | None = None, space: str | None = None):
316
+ def switch(
317
+ registry: Literal["branch", "space"] | None,
318
+ name: str | None,
319
+ branch: str | None,
320
+ space: str | None,
321
+ ):
268
322
  """Switch between branches or spaces.
269
323
 
324
+ Python/R sessions and CLI commands will use the current default branch or space, for example:
325
+
270
326
  ```
271
- lamin switch --branch my_branch
272
- lamin switch --space our_space
327
+ lamin switch branch my_branch
328
+ lamin switch space our_space
273
329
  ```
330
+
331
+ → Python/R alternative: {attr}`~lamindb.setup.core.SetupSettings.branch` and {attr}`~lamindb.setup.core.SetupSettings.space`
274
332
  """
333
+ if registry is not None and name is not None:
334
+ branch = name if registry == "branch" else None
335
+ space = name if registry == "space" else None
336
+ elif branch is None and space is None:
337
+ raise click.UsageError(
338
+ "Specify branch or space. Examples: lamin switch branch my_branch, lamin switch space our_space"
339
+ )
340
+ else:
341
+ warnings.warn(
342
+ "lamin switch --branch and --space are deprecated; use 'lamin switch branch <name>' or 'lamin switch space <name>' instead.",
343
+ DeprecationWarning,
344
+ stacklevel=2,
345
+ )
346
+
275
347
  from lamindb.setup import switch as switch_
276
348
 
277
349
  switch_(branch=branch, space=space)
278
350
 
279
351
 
280
352
  @main.command()
281
- @click.option("--schema", is_flag=True, help="View database schema.")
353
+ @click.option("--schema", is_flag=True, help="View database schema via Django plugin.")
282
354
  def info(schema: bool):
283
- """Show info about the environment, instance, branch, space, and user.
355
+ """Show info about the instance, development & cache directories, branch, space, and user.
284
356
 
285
- See also: Print the instance settings in a Python session via {func}`~lamindb.setup.settings`.
357
+ Manage settings via [lamin settings](https://docs.lamin.ai/cli#settings).
358
+
359
+ → Python/R alternative: {func}`~lamindb.setup.settings`
286
360
  """
287
361
  if schema:
288
362
  from lamindb_setup._schema import view
@@ -297,31 +371,42 @@ def info(schema: bool):
297
371
 
298
372
  # fmt: off
299
373
  @main.command()
374
+ # entity can be a registry or an object in the registry
300
375
  @click.argument("entity", type=str)
301
376
  @click.option("--name", type=str, default=None)
302
377
  @click.option("--uid", type=str, default=None)
303
- @click.option("--slug", type=str, default=None)
378
+ @click.option("--key", type=str, default=None, help="The key for the entity (artifact, transform).")
304
379
  @click.option("--permanent", is_flag=True, default=None, help="Permanently delete the entity where applicable, e.g., for artifact, transform, collection.")
305
380
  @click.option("--force", is_flag=True, default=False, help="Do not ask for confirmation (only relevant for instance).")
306
381
  # fmt: on
307
- def delete(entity: str, name: str | None = None, uid: str | None = None, slug: str | None = None, permanent: bool | None = None, force: bool = False):
308
- """Delete an entity.
382
+ def delete(entity: str, name: str | None = None, uid: str | None = None, key: str | None = None, slug: str | None = None, permanent: bool | None = None, force: bool = False):
383
+ """Delete an object.
309
384
 
310
385
  Currently supported: `branch`, `artifact`, `transform`, `collection`, and `instance`. For example:
311
386
 
312
387
  ```
313
- lamin delete https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5
314
- lamin delete https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5 --permanent
388
+ # via --key or --name
389
+ lamin delete artifact --key mydatasets/mytable.parquet
390
+ lamin delete transform --key myanalyses/analysis.ipynb
315
391
  lamin delete branch --name my_branch
316
392
  lamin delete instance --slug account/name
393
+ # via registry and --uid
394
+ lamin delete artifact --uid e2G7k9EVul4JbfsE
395
+ lamin delete transform --uid Vul4JbfsEYAy5
396
+ # via URL
397
+ lamin delete https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5
398
+ lamin delete https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5 --permanent
317
399
  ```
400
+
401
+ → Python/R alternative: {method}`~lamindb.SQLRecord.delete` and {func}`~lamindb.setup.delete`
318
402
  """
319
403
  from lamin_cli._delete import delete as delete_
320
404
 
321
- return delete_(entity=entity, name=name, uid=uid, slug=slug, permanent=permanent, force=force)
405
+ return delete_(entity=entity, name=name, uid=uid, key=key, permanent=permanent, force=force)
322
406
 
323
407
 
324
408
  @main.command()
409
+ # entity can be a registry or an object in the registry
325
410
  @click.argument("entity", type=str, required=False)
326
411
  @click.option("--uid", help="The uid for the entity.")
327
412
  @click.option("--key", help="The key for the entity.")
@@ -329,23 +414,23 @@ def delete(entity: str, name: str | None = None, uid: str | None = None, slug: s
329
414
  "--with-env", is_flag=True, help="Also return the environment for a tranform."
330
415
  )
331
416
  def load(entity: str | None = None, uid: str | None = None, key: str | None = None, with_env: bool = False):
332
- """Load a file or folder into the cache or working directory.
417
+ """Sync a file/folder into a local cache (artifacts) or development directory (transforms).
333
418
 
334
419
  Pass a URL or `--key`. For example:
335
420
 
336
421
  ```
337
- lamin load https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsE
422
+ # via key
338
423
  lamin load --key mydatasets/mytable.parquet
339
424
  lamin load --key analysis.ipynb
340
425
  lamin load --key myanalyses/analysis.ipynb --with-env
341
- ```
342
-
343
- You can also pass a uid and the entity type:
344
-
345
- ```
426
+ # via registry and --uid
346
427
  lamin load artifact --uid e2G7k9EVul4JbfsE
347
428
  lamin load transform --uid Vul4JbfsEYAy5
429
+ # via URL
430
+ lamin load https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsE
348
431
  ```
432
+
433
+ → Python/R alternative: {func}`~lamindb.Artifact.load`, no equivalent for transforms
349
434
  """
350
435
  from lamin_cli._load import load as load_
351
436
  if entity is not None:
@@ -377,30 +462,36 @@ def _describe(entity: str = "artifact", uid: str | None = None, key: str | None
377
462
 
378
463
 
379
464
  @main.command()
465
+ # entity can be a registry or an object in the registry
380
466
  @click.argument("entity", type=str, default="artifact")
381
467
  @click.option("--uid", help="The uid for the entity.")
382
468
  @click.option("--key", help="The key for the entity.")
383
469
  def describe(entity: str = "artifact", uid: str | None = None, key: str | None = None):
384
- """Describe an artifact.
470
+ """Describe an object.
385
471
 
386
472
  Examples:
387
473
 
388
474
  ```
475
+ # via --key
389
476
  lamin describe --key example_datasets/mini_immuno/dataset1.h5ad
477
+ # via registry and --uid
478
+ lamin describe artifact --uid e2G7k9EVul4JbfsE
479
+ # via URL
390
480
  lamin describe https://lamin.ai/laminlabs/lamin-site-assets/artifact/6sofuDVvTANB0f48
391
481
  ```
392
482
 
393
- See also: Describe an artifact in a Python session via {func}`~lamindb.Artifact.describe`.
483
+ Python/R alternative: {meth}`~lamindb.Artifact.describe`
394
484
  """
395
485
  _describe(entity=entity, uid=uid, key=key)
396
486
 
397
487
 
398
488
  @main.command()
489
+ # entity can be a registry or an object in the registry
399
490
  @click.argument("entity", type=str, default="artifact")
400
491
  @click.option("--uid", help="The uid for the entity.")
401
492
  @click.option("--key", help="The key for the entity.")
402
493
  def get(entity: str = "artifact", uid: str | None = None, key: str | None = None):
403
- """Query metadata about an entity.
494
+ """Query metadata about an object.
404
495
 
405
496
  Currently equivalent to `lamin describe`.
406
497
  """
@@ -416,11 +507,25 @@ def get(entity: str = "artifact", uid: str | None = None, key: str | None = None
416
507
  @click.option("--project", type=str, default=None, help="A valid project name or uid.")
417
508
  @click.option("--space", type=str, default=None, help="A valid space name or uid.")
418
509
  @click.option("--branch", type=str, default=None, help="A valid branch name or uid.")
419
- @click.option("--registry", type=str, default=None, help="Either 'artifact' or 'transform'. If not passed, chooses based on path suffix.")
420
- def save(path: str, key: str, description: str, stem_uid: str, project: str, space: str, branch: str, registry: str):
421
- """Save a file or folder.
510
+ @click.option(
511
+ "--registry",
512
+ type=click.Choice(["artifact", "transform"]),
513
+ default=None,
514
+ help="Either 'artifact' or 'transform'. If not passed, chooses based on path suffix.",
515
+ )
516
+ def save(
517
+ path: str,
518
+ key: str,
519
+ description: str,
520
+ stem_uid: str,
521
+ project: str,
522
+ space: str,
523
+ branch: str,
524
+ registry: Literal["artifact", "transform"] | None,
525
+ ):
526
+ """Save a file or folder as an artifact or transform.
422
527
 
423
- Example: Given a valid project name "my_project",
528
+ Example:
424
529
 
425
530
  ```
426
531
  lamin save my_table.csv --key my_tables/my_table.csv --project my_project
@@ -429,61 +534,124 @@ def save(path: str, key: str, description: str, stem_uid: str, project: str, spa
429
534
  By passing a `--project` identifier, the artifact will be labeled with the corresponding project.
430
535
  If you pass a `--space` or `--branch` identifier, you save the artifact in the corresponding {class}`~lamindb.Space` or on the corresponding {class}`~lamindb.Branch`.
431
536
 
432
- Note: Defaults to saving `.py`, `.ipynb`, `.R`, `.Rmd`, and `.qmd` as {class}`~lamindb.Transform` and
537
+ Defaults to saving `.py`, `.ipynb`, `.R`, `.Rmd`, and `.qmd` as {class}`~lamindb.Transform` and
433
538
  other file types and folders as {class}`~lamindb.Artifact`. You can enforce saving a file as
434
539
  an {class}`~lamindb.Artifact` by passing `--registry artifact`.
540
+
541
+ → Python/R alternative: {class}`~lamindb.Artifact` and {class}`~lamindb.Transform`
435
542
  """
436
543
  if save_(path=path, key=key, description=description, stem_uid=stem_uid, project=project, space=space, branch=branch, registry=registry) is not None:
437
544
  sys.exit(1)
438
545
 
546
+ @main.command()
547
+ def track():
548
+ """Start tracking a run of a shell script.
549
+
550
+ This command works like {func}`~lamindb.track()` in a Python session. Here is an example script:
551
+
552
+ ```
553
+ # my_script.sh
554
+ set -e # exit on error
555
+ lamin track # initiate a tracked shell script run
556
+ lamin load --key raw/file1.txt
557
+ # do something
558
+ lamin save processed_file1.txt --key processed/file1.txt
559
+ lamin finish # mark the shell script run as finished
560
+ ```
561
+
562
+ If you run that script, it will track the run of the script, and save the input and output artifacts:
563
+
564
+ ```
565
+ sh my_script.sh
566
+ ```
567
+
568
+ → Python/R alternative: {func}`~lamindb.track` and {func}`~lamindb.finish` for (non-shell) scripts or notebooks
569
+ """
570
+ from lamin_cli._context import track as track_
571
+ return track_()
572
+
439
573
 
440
574
  @main.command()
575
+ def finish():
576
+ """Finish a currently tracked run of a shell script.
577
+
578
+ → Python/R alternative: {func}`~lamindb.finish()`
579
+ """
580
+ from lamin_cli._context import finish as finish_
581
+ return finish_()
582
+
583
+
584
+ @main.command()
585
+ # entity can be a registry or an object in the registry
441
586
  @click.argument("entity", type=str, default=None, required=False)
442
587
  @click.option("--key", type=str, default=None, help="The key of an artifact or transform.")
443
588
  @click.option("--uid", type=str, default=None, help="The uid of an artifact or transform.")
444
589
  @click.option("--project", type=str, default=None, help="A valid project name or uid.")
590
+ @click.option("--ulabel", type=str, default=None, help="A valid ulabel name or uid.")
591
+ @click.option("--record", type=str, default=None, help="A valid record name or uid.")
445
592
  @click.option("--features", multiple=True, help="Feature annotations. Supports: feature=value, feature=val1,val2, or feature=\"val1\",\"val2\"")
446
- def annotate(entity: str | None, key: str, uid: str, project: str, features: tuple):
593
+ def annotate(entity: str | None, key: str, uid: str, project: str, ulabel: str, record: str, features: tuple):
447
594
  """Annotate an artifact or transform.
448
595
 
449
- Entity is either 'artifact' or 'transform'. If not passed, chooses based on key suffix.
450
-
451
- You can annotate with projects and valid features & values. For example,
596
+ You can annotate with projects, ulabels, records, and valid features & values. For example,
452
597
 
453
598
  ```
599
+ # via --key
454
600
  lamin annotate --key raw/sample.fastq --project "My Project"
601
+ lamin annotate --key raw/sample.fastq --ulabel "My ULabel" --record "Experiment 1"
455
602
  lamin annotate --key raw/sample.fastq --features perturbation=IFNG,DMSO cell_line=HEK297
456
603
  lamin annotate --key my-notebook.ipynb --project "My Project"
604
+ # via registry and --uid
605
+ lamin annotate artifact --uid e2G7k9EVul4JbfsE --project "My Project"
606
+ # via URL
607
+ lamin annotate https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsE --project "My Project"
457
608
  ```
458
- """
459
- import lamindb as ln
460
609
 
610
+ → Python/R alternative: `artifact.features.add_values()` via {meth}`~lamindb.models.FeatureManager.add_values` and `artifact.projects.add()`, `artifact.ulabels.add()`, `artifact.records.add()`, ... via {meth}`~lamindb.models.RelatedManager.add`
611
+ """
461
612
  from lamin_cli._annotate import _parse_features_list
462
613
  from lamin_cli._save import infer_registry_from_path
463
614
 
464
- # once we enable passing the URL as entity, then we don't need to throw this error
465
- if not ln.setup.settings._instance_exists:
466
- raise click.ClickException("Not connected to an instance. Please run: lamin connect account/name")
467
-
468
- if entity is None:
469
- if key is not None:
470
- registry = infer_registry_from_path(key)
471
- else:
472
- registry = "artifact"
615
+ # Handle URL: decompose and connect (same pattern as load/delete)
616
+ if entity is not None and entity.startswith("https://"):
617
+ url = entity
618
+ instance, registry, uid = decompose_url(url)
619
+ if registry not in {"artifact", "transform"}:
620
+ raise click.ClickException(
621
+ f"Annotate does not support {registry}. Use artifact or transform URLs."
622
+ )
623
+ ln_setup.connect(instance)
473
624
  else:
474
- registry = entity
625
+ if not ln_setup.settings._instance_exists:
626
+ raise click.ClickException(
627
+ "Not connected to an instance. Please run: lamin connect account/name"
628
+ )
629
+ if entity is None:
630
+ registry = infer_registry_from_path(key) if key is not None else "artifact"
631
+ else:
632
+ registry = entity
633
+ if registry not in {"artifact", "transform"}:
634
+ raise click.ClickException(
635
+ f"Annotate does not support {registry}. Use artifact or transform URLs."
636
+ )
637
+
638
+ # import lamindb after connect went through
639
+ import lamindb as ln
640
+
475
641
  if registry == "artifact":
476
642
  model = ln.Artifact
477
643
  else:
478
644
  model = ln.Transform
479
645
 
480
- # Get the artifact
646
+ # Get the artifact or transform
481
647
  if key is not None:
482
648
  artifact = model.get(key=key)
483
649
  elif uid is not None:
484
650
  artifact = model.get(uid) # do not use uid=uid, because then no truncated uids would work
485
651
  else:
486
- raise ln.errors.InvalidArgument("Either --key or --uid must be provided")
652
+ raise ln.errors.InvalidArgument(
653
+ "Either pass a URL as entity or provide --key or --uid"
654
+ )
487
655
 
488
656
  # Handle project annotation
489
657
  if project is not None:
@@ -496,6 +664,28 @@ def annotate(entity: str | None, key: str, uid: str, project: str, features: tup
496
664
  )
497
665
  artifact.projects.add(project_record)
498
666
 
667
+ # Handle ulabel annotation
668
+ if ulabel is not None:
669
+ ulabel_record = ln.ULabel.filter(
670
+ ln.Q(name=ulabel) | ln.Q(uid=ulabel)
671
+ ).one_or_none()
672
+ if ulabel_record is None:
673
+ raise ln.errors.InvalidArgument(
674
+ f"ULabel '{ulabel}' not found, either create it with `ln.ULabel(name='...').save()` or fix typos."
675
+ )
676
+ artifact.ulabels.add(ulabel_record)
677
+
678
+ # Handle record annotation
679
+ if record is not None:
680
+ record_record = ln.Record.filter(
681
+ ln.Q(name=record) | ln.Q(uid=record)
682
+ ).one_or_none()
683
+ if record_record is None:
684
+ raise ln.errors.InvalidArgument(
685
+ f"Record '{record}' not found, either create it with `ln.Record(name='...').save()` or fix typos."
686
+ )
687
+ artifact.records.add(record_record)
688
+
499
689
  # Handle feature annotations
500
690
  if features:
501
691
  feature_dict = _parse_features_list(features)
@@ -509,7 +699,7 @@ def annotate(entity: str | None, key: str, uid: str, project: str, features: tup
509
699
  @click.argument("filepath", type=str)
510
700
  @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)
511
701
  @click.option("--image-url", type=str, default=None, help="A URL to the base docker image to use.")
512
- @click.option("--packages", type=str, default="lamindb", help="A comma-separated list of additional packages to install.")
702
+ @click.option("--packages", type=str, default=None, help="A comma-separated list of additional packages to install.")
513
703
  @click.option("--cpu", type=float, default=None, help="Configuration for the CPU.")
514
704
  @click.option("--gpu", type=str, default=None, help="The type of GPU to use (only compatible with cuda images).")
515
705
  def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gpu: str | None):
@@ -522,6 +712,8 @@ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gp
522
712
  ```
523
713
  lamin run my_script.py --project my_project
524
714
  ```
715
+
716
+ → Python/R alternative: no equivalent
525
717
  """
526
718
  from lamin_cli.compute.modal import Runner
527
719
 
@@ -550,10 +742,54 @@ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gp
550
742
 
551
743
 
552
744
  main.add_command(settings)
553
- main.add_command(cache)
554
745
  main.add_command(migrate)
555
746
  main.add_command(io)
556
747
 
748
+
749
+ def _deprecated_cache_set(cache_dir: str) -> None:
750
+ logger.warning("'lamin cache' is deprecated. Use 'lamin settings cache-dir' instead.")
751
+ from lamindb_setup._cache import set_cache_dir
752
+
753
+ set_cache_dir(cache_dir)
754
+
755
+
756
+ def _deprecated_cache_clear() -> None:
757
+ logger.warning("'lamin cache' is deprecated. Use 'lamin settings cache-dir' instead.")
758
+ from lamindb_setup._cache import clear_cache_dir
759
+
760
+ clear_cache_dir()
761
+
762
+
763
+ def _deprecated_cache_get() -> None:
764
+ logger.warning("'lamin cache' is deprecated. Use 'lamin settings cache-dir' instead.")
765
+ from lamindb_setup._cache import get_cache_dir
766
+
767
+ click.echo(f"The cache directory is {get_cache_dir()}")
768
+
769
+
770
+ @main.group("cache", hidden=True)
771
+ def deprecated_cache():
772
+ """Deprecated. Use 'lamin settings cache-dir' instead."""
773
+
774
+
775
+ @deprecated_cache.command("set")
776
+ @click.argument(
777
+ "cache_dir",
778
+ type=click.Path(dir_okay=True, file_okay=False),
779
+ )
780
+ def _deprecated_cache_set_cmd(cache_dir: str) -> None:
781
+ _deprecated_cache_set(cache_dir)
782
+
783
+
784
+ @deprecated_cache.command("clear")
785
+ def _deprecated_cache_clear_cmd() -> None:
786
+ _deprecated_cache_clear()
787
+
788
+
789
+ @deprecated_cache.command("get")
790
+ def _deprecated_cache_get_cmd() -> None:
791
+ _deprecated_cache_get()
792
+
557
793
  # https://stackoverflow.com/questions/57810659/automatically-generate-all-help-documentation-for-click-commands
558
794
  # https://claude.ai/chat/73c28487-bec3-4073-8110-50d1a2dd6b84
559
795
  def _generate_help():
@@ -562,6 +798,8 @@ def _generate_help():
562
798
  def recursive_help(
563
799
  cmd: Command, parent: Context | None = None, name: tuple[str, ...] = ()
564
800
  ):
801
+ if getattr(cmd, "hidden", False):
802
+ return
565
803
  ctx = click.Context(cmd, info_name=cmd.name, parent=parent)
566
804
  assert cmd.name
567
805
  name = (*name, cmd.name)
@@ -576,6 +814,8 @@ def _generate_help():
576
814
  }
577
815
 
578
816
  for sub in getattr(cmd, "commands", {}).values():
817
+ if getattr(sub, "hidden", False):
818
+ continue
579
819
  recursive_help(sub, ctx, name=name)
580
820
 
581
821
  recursive_help(main)