lamin_cli 1.9.0__tar.gz → 1.11.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.
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/.github/workflows/build.yml +3 -3
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/.gitignore +3 -9
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/.pre-commit-config.yaml +5 -5
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/PKG-INFO +1 -1
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/__init__.py +12 -3
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/__main__.py +71 -18
- lamin_cli-1.11.0/lamin_cli/_context.py +76 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/_delete.py +5 -4
- lamin_cli-1.11.0/lamin_cli/_io.py +144 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/_load.py +42 -21
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/_migration.py +0 -1
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/_save.py +57 -32
- lamin_cli-1.11.0/lamin_cli/clone/_clone_verification.py +56 -0
- lamin_cli-1.11.0/lamin_cli/clone/create_sqlite_clone_and_import_db.py +51 -0
- lamin_cli-1.11.0/lamin_cli/compute/__init__.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_create_switch_delete_list_settings.py +5 -5
- lamin_cli-1.11.0/tests/core/test_io.py +48 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_load.py +11 -8
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_login.py +0 -16
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_save_annotate_scripts.py +4 -1
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_save_notebooks.py +4 -1
- lamin_cli-1.11.0/tests/core/test_save_shell_script.py +38 -0
- lamin_cli-1.11.0/tests/core/test_track.py +65 -0
- lamin_cli-1.11.0/tests/scripts/track-lineage.sh +15 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/.github/workflows/doc-changes.yml +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/LICENSE +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/README.md +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/_annotate.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/_cache.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/_settings.py +0 -0
- {lamin_cli-1.9.0/lamin_cli/compute → lamin_cli-1.11.0/lamin_cli/clone}/__init__.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/compute/modal.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/lamin_cli/urls.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/noxfile.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/pyproject.toml +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/conftest.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_migrate.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_multi_process.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_parse_uid_from_code.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_save_annotate_files.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/core/test_save_r_code.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/modal/test_modal.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/notebooks/not-initialized.ipynb +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/notebooks/with-title-and-initialized-consecutive.ipynb +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/notebooks/with-title-and-initialized-non-consecutive.ipynb +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/scripts/merely-import-lamindb.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/scripts/run-track-and-finish-sync-git.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/scripts/run-track-and-finish.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/scripts/run-track-with-params.py +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/scripts/run-track.R +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/scripts/run-track.qmd +0 -0
- {lamin_cli-1.9.0 → lamin_cli-1.11.0}/tests/scripts/testscript.py +0 -0
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
outputs:
|
|
12
12
|
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
|
13
13
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
14
|
+
- uses: actions/checkout@v5
|
|
15
15
|
with:
|
|
16
16
|
fetch-depth: 0
|
|
17
17
|
|
|
@@ -54,12 +54,12 @@ jobs:
|
|
|
54
54
|
matrix: ${{fromJson(needs.pre-filter.outputs.matrix)}}
|
|
55
55
|
timeout-minutes: 20
|
|
56
56
|
steps:
|
|
57
|
-
- uses: actions/checkout@
|
|
57
|
+
- uses: actions/checkout@v5
|
|
58
58
|
with:
|
|
59
59
|
submodules: recursive
|
|
60
60
|
fetch-depth: 0
|
|
61
61
|
|
|
62
|
-
- uses: actions/setup-python@
|
|
62
|
+
- uses: actions/setup-python@v6
|
|
63
63
|
with:
|
|
64
64
|
python-version: 3.12
|
|
65
65
|
|
|
@@ -22,7 +22,6 @@ docs/conf.py
|
|
|
22
22
|
lamindb/setup/.env
|
|
23
23
|
_secrets.py
|
|
24
24
|
_configuration.py
|
|
25
|
-
lamin.db
|
|
26
25
|
docs/generated/*
|
|
27
26
|
_docs_tmp*
|
|
28
27
|
docs/guide/Laminopathic_nuclei.jpg
|
|
@@ -41,14 +40,6 @@ docs/biology/test-flow/
|
|
|
41
40
|
docs/biology/test-scrna/
|
|
42
41
|
docs/biology/test-registries/
|
|
43
42
|
docs/biology/test-multimodal/
|
|
44
|
-
test-inherit1
|
|
45
|
-
test-inherit2
|
|
46
|
-
test-search0
|
|
47
|
-
test-search1
|
|
48
|
-
test-search2
|
|
49
|
-
test-search3
|
|
50
|
-
test-search4
|
|
51
|
-
test-search5
|
|
52
43
|
test.ipynb
|
|
53
44
|
|
|
54
45
|
# General
|
|
@@ -181,6 +172,9 @@ venv.bak/
|
|
|
181
172
|
.dmypy.json
|
|
182
173
|
dmypy.json
|
|
183
174
|
|
|
175
|
+
# ruff
|
|
176
|
+
.ruff_cache
|
|
177
|
+
|
|
184
178
|
# Pyre type checker
|
|
185
179
|
.pyre/
|
|
186
180
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
repos:
|
|
2
2
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
-
rev:
|
|
3
|
+
rev: v6.0.0
|
|
4
4
|
hooks:
|
|
5
5
|
- id: trailing-whitespace
|
|
6
6
|
- id: end-of-file-fixer
|
|
@@ -11,13 +11,13 @@ repos:
|
|
|
11
11
|
- id: check-yaml
|
|
12
12
|
- id: check-added-large-files
|
|
13
13
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
14
|
-
rev: v0.
|
|
14
|
+
rev: v0.14.6
|
|
15
15
|
hooks:
|
|
16
|
-
- id: ruff
|
|
16
|
+
- id: ruff-check
|
|
17
17
|
args: [--fix, --exit-non-zero-on-fix, --unsafe-fixes]
|
|
18
18
|
- id: ruff-format
|
|
19
19
|
- repo: https://github.com/rbubley/mirrors-prettier
|
|
20
|
-
rev: v3.
|
|
20
|
+
rev: v3.6.2
|
|
21
21
|
hooks:
|
|
22
22
|
- id: prettier
|
|
23
23
|
- repo: https://github.com/kynan/nbstripout
|
|
@@ -31,7 +31,7 @@ repos:
|
|
|
31
31
|
tests
|
|
32
32
|
)
|
|
33
33
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
34
|
-
rev: v1.
|
|
34
|
+
rev: v1.18.2
|
|
35
35
|
hooks:
|
|
36
36
|
- id: mypy
|
|
37
37
|
exclude: |
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This is the command line interface for interacting with LaminDB & LaminHub.
|
|
4
4
|
|
|
5
|
-
The interface is defined in `__main__.py`.
|
|
5
|
+
The interface is defined in `__main__.py`.
|
|
6
|
+
The root API here is used by LaminR to replicate the CLI functionality.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
|
-
__version__ = "1.
|
|
9
|
+
__version__ = "1.11.0"
|
|
9
10
|
|
|
10
11
|
from lamindb_setup import disconnect, logout
|
|
11
12
|
from lamindb_setup._connect_instance import _connect_cli as connect
|
|
@@ -15,4 +16,12 @@ from lamindb_setup._setup_user import login
|
|
|
15
16
|
from ._delete import delete
|
|
16
17
|
from ._save import save
|
|
17
18
|
|
|
18
|
-
__all__ = [
|
|
19
|
+
__all__ = [
|
|
20
|
+
"save",
|
|
21
|
+
"init",
|
|
22
|
+
"connect",
|
|
23
|
+
"delete",
|
|
24
|
+
"login",
|
|
25
|
+
"logout",
|
|
26
|
+
"disconnect",
|
|
27
|
+
]
|
|
@@ -42,13 +42,23 @@ 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"],
|
|
48
52
|
},
|
|
49
53
|
{
|
|
50
54
|
"name": "Configure",
|
|
51
|
-
"commands": [
|
|
55
|
+
"commands": [
|
|
56
|
+
"checkout",
|
|
57
|
+
"switch",
|
|
58
|
+
"cache",
|
|
59
|
+
"settings",
|
|
60
|
+
"migrate",
|
|
61
|
+
],
|
|
52
62
|
},
|
|
53
63
|
{
|
|
54
64
|
"name": "Auth",
|
|
@@ -103,6 +113,7 @@ else:
|
|
|
103
113
|
from lamindb_setup._silence_loggers import silence_loggers
|
|
104
114
|
|
|
105
115
|
from lamin_cli._cache import cache
|
|
116
|
+
from lamin_cli._io import io
|
|
106
117
|
from lamin_cli._migration import migrate
|
|
107
118
|
from lamin_cli._settings import settings
|
|
108
119
|
|
|
@@ -294,54 +305,60 @@ def info(schema: bool):
|
|
|
294
305
|
@click.option("--name", type=str, default=None)
|
|
295
306
|
@click.option("--uid", type=str, default=None)
|
|
296
307
|
@click.option("--slug", type=str, default=None)
|
|
308
|
+
@click.option("--permanent", is_flag=True, default=None, help="Permanently delete the entity where applicable, e.g., for artifact, transform, collection.")
|
|
297
309
|
@click.option("--force", is_flag=True, default=False, help="Do not ask for confirmation (only relevant for instance).")
|
|
298
310
|
# fmt: on
|
|
299
|
-
def delete(entity: str, name: str | None = None, uid: str | None = None, slug: str | None = None, force: bool = False):
|
|
311
|
+
def delete(entity: str, name: str | None = None, uid: str | None = None, slug: str | None = None, permanent: bool | None = None, force: bool = False):
|
|
300
312
|
"""Delete an entity.
|
|
301
313
|
|
|
302
314
|
Currently supported: `branch`, `artifact`, `transform`, `collection`, and `instance`. For example:
|
|
303
315
|
|
|
304
316
|
```
|
|
305
317
|
lamin delete https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5
|
|
318
|
+
lamin delete https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsEYAy5 --permanent
|
|
306
319
|
lamin delete branch --name my_branch
|
|
307
320
|
lamin delete instance --slug account/name
|
|
308
321
|
```
|
|
309
322
|
"""
|
|
310
323
|
from lamin_cli._delete import delete as delete_
|
|
311
324
|
|
|
312
|
-
return delete_(entity=entity, name=name, uid=uid, slug=slug, force=force)
|
|
325
|
+
return delete_(entity=entity, name=name, uid=uid, slug=slug, permanent=permanent, force=force)
|
|
313
326
|
|
|
314
327
|
|
|
315
328
|
@main.command()
|
|
316
|
-
@click.argument("entity", type=str)
|
|
329
|
+
@click.argument("entity", type=str, required=False)
|
|
317
330
|
@click.option("--uid", help="The uid for the entity.")
|
|
318
331
|
@click.option("--key", help="The key for the entity.")
|
|
319
332
|
@click.option(
|
|
320
333
|
"--with-env", is_flag=True, help="Also return the environment for a tranform."
|
|
321
334
|
)
|
|
322
|
-
def load(entity: str, uid: str | None = None, key: str | None = None, with_env: bool = False):
|
|
335
|
+
def load(entity: str | None = None, uid: str | None = None, key: str | None = None, with_env: bool = False):
|
|
323
336
|
"""Load a file or folder into the cache or working directory.
|
|
324
337
|
|
|
325
|
-
Pass a URL
|
|
338
|
+
Pass a URL or `--key`. For example:
|
|
326
339
|
|
|
327
340
|
```
|
|
328
341
|
lamin load https://lamin.ai/account/instance/artifact/e2G7k9EVul4JbfsE
|
|
329
|
-
lamin load
|
|
342
|
+
lamin load --key mydatasets/mytable.parquet
|
|
343
|
+
lamin load --key analysis.ipynb
|
|
344
|
+
lamin load --key myanalyses/analysis.ipynb --with-env
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
You can also pass a uid and the entity type:
|
|
348
|
+
|
|
349
|
+
```
|
|
330
350
|
lamin load artifact --uid e2G7k9EVul4JbfsE
|
|
331
|
-
lamin load transform --key analysis.ipynb
|
|
332
351
|
lamin load transform --uid Vul4JbfsEYAy5
|
|
333
|
-
lamin load transform --uid Vul4JbfsEYAy5 --with-env
|
|
334
352
|
```
|
|
335
353
|
"""
|
|
336
|
-
|
|
337
|
-
if
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return load_(entity, uid=uid, key=key, with_env=with_env)
|
|
354
|
+
from lamin_cli._load import load as load_
|
|
355
|
+
if entity is not None:
|
|
356
|
+
is_slug = entity.count("/") == 1
|
|
357
|
+
if is_slug:
|
|
358
|
+
from lamindb_setup._connect_instance import _connect_cli
|
|
359
|
+
# for backward compat
|
|
360
|
+
return _connect_cli(entity)
|
|
361
|
+
return load_(entity, uid=uid, key=key, with_env=with_env)
|
|
345
362
|
|
|
346
363
|
|
|
347
364
|
def _describe(entity: str = "artifact", uid: str | None = None, key: str | None = None):
|
|
@@ -423,6 +440,41 @@ def save(path: str, key: str, description: str, stem_uid: str, project: str, spa
|
|
|
423
440
|
if save_(path=path, key=key, description=description, stem_uid=stem_uid, project=project, space=space, branch=branch, registry=registry) is not None:
|
|
424
441
|
sys.exit(1)
|
|
425
442
|
|
|
443
|
+
@main.command()
|
|
444
|
+
def track():
|
|
445
|
+
"""Start tracking a run of a shell script.
|
|
446
|
+
|
|
447
|
+
This command works like {func}`~lamindb.track()` in a Python session. Here is an example script:
|
|
448
|
+
|
|
449
|
+
```
|
|
450
|
+
# my_script.sh
|
|
451
|
+
set -e # exit on error
|
|
452
|
+
lamin track # initiate a tracked shell script run
|
|
453
|
+
lamin load --key raw/file1.txt
|
|
454
|
+
# do something
|
|
455
|
+
lamin save processed_file1.txt --key processed/file1.txt
|
|
456
|
+
lamin finish # mark the shell script run as finished
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
If you run that script, it will track the run of the script, and save the input and output artifacts:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
sh my_script.sh
|
|
463
|
+
```
|
|
464
|
+
"""
|
|
465
|
+
from lamin_cli._context import track as track_
|
|
466
|
+
return track_()
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@main.command()
|
|
470
|
+
def finish():
|
|
471
|
+
"""Finish the current tracked run of a shell script.
|
|
472
|
+
|
|
473
|
+
This command works like {func}`~lamindb.finish()` in a Python session.
|
|
474
|
+
"""
|
|
475
|
+
from lamin_cli._context import finish as finish_
|
|
476
|
+
return finish_()
|
|
477
|
+
|
|
426
478
|
|
|
427
479
|
@main.command()
|
|
428
480
|
@click.argument("entity", type=str, default=None, required=False)
|
|
@@ -539,6 +591,7 @@ def run(filepath: str, project: str, image_url: str, packages: str, cpu: int, gp
|
|
|
539
591
|
main.add_command(settings)
|
|
540
592
|
main.add_command(cache)
|
|
541
593
|
main.add_command(migrate)
|
|
594
|
+
main.add_command(io)
|
|
542
595
|
|
|
543
596
|
# https://stackoverflow.com/questions/57810659/automatically-generate-all-help-documentation-for-click-commands
|
|
544
597
|
# https://claude.ai/chat/73c28487-bec3-4073-8110-50d1a2dd6b84
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from lamin_utils import logger
|
|
7
|
+
from lamindb_setup.core._settings_store import settings_dir
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_current_run_file() -> Path:
|
|
11
|
+
"""Get the path to the file storing the current run UID."""
|
|
12
|
+
return settings_dir / "current_shell_run.txt"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_interactive_shell() -> bool:
|
|
16
|
+
"""Check if running in an interactive terminal."""
|
|
17
|
+
return sys.stdin.isatty() and sys.stdout.isatty() and os.isatty(0)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_script_filename() -> Path:
|
|
21
|
+
"""Try to get the filename of the calling shell script."""
|
|
22
|
+
import psutil
|
|
23
|
+
|
|
24
|
+
parent = psutil.Process(os.getppid())
|
|
25
|
+
cmdline = parent.cmdline()
|
|
26
|
+
|
|
27
|
+
# For shells like bash, sh, zsh
|
|
28
|
+
if parent.name() in ["bash", "sh", "zsh", "dash"]:
|
|
29
|
+
# cmdline is typically: ['/bin/bash', 'script.sh', ...]
|
|
30
|
+
if len(cmdline) > 1 and not cmdline[1].startswith("-"):
|
|
31
|
+
return Path(cmdline[1])
|
|
32
|
+
raise click.ClickException(
|
|
33
|
+
"Cannot determine script filename. Please run in an interactive shell."
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def track():
|
|
38
|
+
import lamindb as ln
|
|
39
|
+
|
|
40
|
+
if not ln.setup.settings._instance_exists:
|
|
41
|
+
raise click.ClickException(
|
|
42
|
+
"Not connected to an instance. Please run: lamin connect account/name"
|
|
43
|
+
)
|
|
44
|
+
path = get_script_filename()
|
|
45
|
+
source_code = path.read_text()
|
|
46
|
+
transform = ln.Transform(
|
|
47
|
+
key=path.name, source_code=source_code, type="script"
|
|
48
|
+
).save()
|
|
49
|
+
run = ln.Run(transform=transform).save()
|
|
50
|
+
current_run_file = get_current_run_file()
|
|
51
|
+
current_run_file.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
current_run_file.write_text(run.uid)
|
|
53
|
+
logger.important(f"started tracking shell run: {run.uid}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def finish():
|
|
57
|
+
from datetime import datetime, timezone
|
|
58
|
+
|
|
59
|
+
import lamindb as ln
|
|
60
|
+
|
|
61
|
+
if not ln.setup.settings._instance_exists:
|
|
62
|
+
raise click.ClickException(
|
|
63
|
+
"Not connected to an instance. Please run: lamin connect account/name"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
current_run_file = get_current_run_file()
|
|
67
|
+
if not current_run_file.exists():
|
|
68
|
+
raise click.ClickException(
|
|
69
|
+
"No active run to finish. Please run `lamin track` first."
|
|
70
|
+
)
|
|
71
|
+
run = ln.Run.get(uid=current_run_file.read_text().strip())
|
|
72
|
+
run._status_code = 0
|
|
73
|
+
run.finished_at = datetime.now(timezone.utc)
|
|
74
|
+
run.save()
|
|
75
|
+
current_run_file.unlink()
|
|
76
|
+
logger.important(f"finished tracking shell run: {run.uid}")
|
|
@@ -9,6 +9,7 @@ def delete(
|
|
|
9
9
|
name: str | None = None,
|
|
10
10
|
uid: str | None = None,
|
|
11
11
|
slug: str | None = None,
|
|
12
|
+
permanent: bool | None = None,
|
|
12
13
|
force: bool = False,
|
|
13
14
|
):
|
|
14
15
|
# TODO: refactor to abstract getting and deleting across entities
|
|
@@ -21,22 +22,22 @@ def delete(
|
|
|
21
22
|
assert name is not None, "You have to pass a name for deleting a branch."
|
|
22
23
|
from lamindb import Branch
|
|
23
24
|
|
|
24
|
-
Branch.get(name=name).delete()
|
|
25
|
+
Branch.get(name=name).delete(permanent=permanent)
|
|
25
26
|
elif entity == "artifact":
|
|
26
27
|
assert uid is not None, "You have to pass a uid for deleting an artifact."
|
|
27
28
|
from lamindb import Artifact
|
|
28
29
|
|
|
29
|
-
Artifact.get(uid).delete()
|
|
30
|
+
Artifact.get(uid).delete(permanent=permanent)
|
|
30
31
|
elif entity == "transform":
|
|
31
32
|
assert uid is not None, "You have to pass a uid for deleting an transform."
|
|
32
33
|
from lamindb import Transform
|
|
33
34
|
|
|
34
|
-
Transform.get(uid).delete()
|
|
35
|
+
Transform.get(uid).delete(permanent=permanent)
|
|
35
36
|
elif entity == "collection":
|
|
36
37
|
assert uid is not None, "You have to pass a uid for deleting an collection."
|
|
37
38
|
from lamindb import Collection
|
|
38
39
|
|
|
39
|
-
Collection.get(uid).delete()
|
|
40
|
+
Collection.get(uid).delete(permanent=permanent)
|
|
40
41
|
elif entity == "instance":
|
|
41
42
|
return delete_instance(slug, force=force)
|
|
42
43
|
else: # backwards compatibility
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import lamindb_setup as ln_setup
|
|
11
|
+
|
|
12
|
+
from lamin_cli.clone._clone_verification import (
|
|
13
|
+
_count_instance_records,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
if os.environ.get("NO_RICH"):
|
|
17
|
+
import click as click
|
|
18
|
+
else:
|
|
19
|
+
import rich_click as click
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group()
|
|
23
|
+
def io():
|
|
24
|
+
"""Import and export instances."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# fmt: off
|
|
28
|
+
@io.command("snapshot")
|
|
29
|
+
@click.option("--upload/--no-upload", is_flag=True, help="Whether to upload the snapshot.", default=True)
|
|
30
|
+
@click.option("--track/--no-track", is_flag=True, help="Whether to track snapshot generation.", default=True)
|
|
31
|
+
# fmt: on
|
|
32
|
+
def snapshot(upload: bool, track: bool) -> None:
|
|
33
|
+
"""Create a SQLite snapshot of the connected instance."""
|
|
34
|
+
if not ln_setup.settings._instance_exists:
|
|
35
|
+
raise click.ClickException(
|
|
36
|
+
"Not connected to an instance. Please run: lamin connect account/name"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
instance_owner = ln_setup.settings.instance.owner
|
|
40
|
+
instance_name = ln_setup.settings.instance.name
|
|
41
|
+
|
|
42
|
+
ln_setup.connect(f"{instance_owner}/{instance_name}", use_root_db_user=True)
|
|
43
|
+
|
|
44
|
+
import lamindb as ln
|
|
45
|
+
|
|
46
|
+
original_counts = _count_instance_records()
|
|
47
|
+
|
|
48
|
+
modules_without_lamindb = ln_setup.settings.instance.modules
|
|
49
|
+
modules_complete = modules_without_lamindb.copy()
|
|
50
|
+
modules_complete.add("lamindb")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
with tempfile.TemporaryDirectory() as export_dir:
|
|
54
|
+
if track:
|
|
55
|
+
ln.track()
|
|
56
|
+
ln_setup.io.export_db(module_names=modules_complete, output_dir=export_dir)
|
|
57
|
+
if track:
|
|
58
|
+
ln.finish()
|
|
59
|
+
|
|
60
|
+
script_path = (
|
|
61
|
+
Path(__file__).parent / "clone" / "create_sqlite_clone_and_import_db.py"
|
|
62
|
+
)
|
|
63
|
+
result = subprocess.run(
|
|
64
|
+
[
|
|
65
|
+
sys.executable,
|
|
66
|
+
str(script_path),
|
|
67
|
+
"--instance-name",
|
|
68
|
+
instance_name,
|
|
69
|
+
"--export-dir",
|
|
70
|
+
export_dir,
|
|
71
|
+
"--modules",
|
|
72
|
+
",".join(modules_without_lamindb),
|
|
73
|
+
"--original-counts",
|
|
74
|
+
json.dumps(original_counts),
|
|
75
|
+
],
|
|
76
|
+
check=False,
|
|
77
|
+
stderr=subprocess.PIPE,
|
|
78
|
+
text=True,
|
|
79
|
+
cwd=Path.cwd(),
|
|
80
|
+
)
|
|
81
|
+
if result.returncode != 0:
|
|
82
|
+
try:
|
|
83
|
+
mismatches = json.loads(result.stderr.strip())
|
|
84
|
+
error_msg = "Record count mismatch detected:\n" + "\n".join(
|
|
85
|
+
[f" {table}: original={orig}, clone={clone}"
|
|
86
|
+
for table, (orig, clone) in mismatches.items()]
|
|
87
|
+
)
|
|
88
|
+
raise click.ClickException(error_msg)
|
|
89
|
+
except (json.JSONDecodeError, AttributeError, ValueError, TypeError):
|
|
90
|
+
raise click.ClickException(f"Clone verification failed:\n{result.stderr}") from None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
ln_setup.connect(f"{instance_owner}/{instance_name}", use_root_db_user=True)
|
|
94
|
+
if upload:
|
|
95
|
+
ln_setup.core._clone.upload_sqlite_clone(
|
|
96
|
+
local_sqlite_path=f"{instance_name}-clone/.lamindb/lamin.db",
|
|
97
|
+
compress=True,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
ln_setup.disconnect()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# fmt: off
|
|
104
|
+
@io.command("exportdb")
|
|
105
|
+
@click.option("--modules", type=str, default=None, help="Comma-separated list of modules to export (e.g., 'lamindb,bionty').",)
|
|
106
|
+
@click.option("--output-dir", type=str, help="Output directory for exported parquet files.")
|
|
107
|
+
@click.option("--max-workers", type=int, default=8, help="Number of parallel workers.")
|
|
108
|
+
@click.option("--chunk-size", type=int, default=500_000, help="Number of rows per chunk for large tables.")
|
|
109
|
+
# fmt: on
|
|
110
|
+
def exportdb(modules: str | None, output_dir: str, max_workers: int, chunk_size: int):
|
|
111
|
+
"""Export registry tables to parquet files."""
|
|
112
|
+
if not ln_setup.settings._instance_exists:
|
|
113
|
+
raise click.ClickException(
|
|
114
|
+
"Not connected to an instance. Please run: lamin connect account/name"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
module_list = modules.split(",") if modules else None
|
|
118
|
+
ln_setup.io.export_db(
|
|
119
|
+
module_names=module_list,
|
|
120
|
+
output_dir=output_dir,
|
|
121
|
+
max_workers=max_workers,
|
|
122
|
+
chunk_size=chunk_size,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# fmt: off
|
|
127
|
+
@io.command("importdb")
|
|
128
|
+
@click.option("--modules", type=str, default=None, help="Comma-separated list of modules to import (e.g., 'lamindb,bionty').")
|
|
129
|
+
@click.option("--input-dir", type=str, help="Input directory containing exported parquet files.")
|
|
130
|
+
@click.option("--if-exists", type=click.Choice(["fail", "replace", "append"]), default="replace", help="How to handle existing data.")
|
|
131
|
+
# fmt: on
|
|
132
|
+
def importdb(modules: str | None, input_dir: str, if_exists: str):
|
|
133
|
+
"""Import registry tables from parquet files."""
|
|
134
|
+
if not ln_setup.settings._instance_exists:
|
|
135
|
+
raise click.ClickException(
|
|
136
|
+
"Not connected to an instance. Please run: lamin connect account/name"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
module_list = modules.split(",") if modules else None
|
|
140
|
+
ln_setup.io.import_db(
|
|
141
|
+
module_names=module_list,
|
|
142
|
+
input_dir=input_dir,
|
|
143
|
+
if_exists=if_exists,
|
|
144
|
+
)
|
|
@@ -6,15 +6,36 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
from lamin_utils import logger
|
|
8
8
|
|
|
9
|
-
from .
|
|
9
|
+
from ._context import get_current_run_file
|
|
10
|
+
from ._save import infer_registry_from_path, parse_title_r_notebook
|
|
10
11
|
from .urls import decompose_url
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
def load(
|
|
14
|
-
entity: str
|
|
15
|
+
entity: str | None = None,
|
|
16
|
+
uid: str | None = None,
|
|
17
|
+
key: str | None = None,
|
|
18
|
+
with_env: bool = False,
|
|
15
19
|
):
|
|
20
|
+
"""Load artifact, collection, or transform from LaminDB.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
entity: URL containing 'lamin', or 'artifact', 'collection', or 'transform'
|
|
24
|
+
uid: Unique identifier (prefix matching supported)
|
|
25
|
+
key: Key identifier
|
|
26
|
+
with_env: If True, also load environment requirements file for transforms
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Path to loaded transform, or None for artifacts/collections
|
|
30
|
+
"""
|
|
16
31
|
import lamindb_setup as ln_setup
|
|
17
32
|
|
|
33
|
+
if entity is None:
|
|
34
|
+
if key is None:
|
|
35
|
+
raise SystemExit("Either entity or key has to be provided.")
|
|
36
|
+
else:
|
|
37
|
+
entity = infer_registry_from_path(key)
|
|
38
|
+
|
|
18
39
|
if entity.startswith("https://") and "lamin" in entity:
|
|
19
40
|
url = entity
|
|
20
41
|
instance, entity, uid = decompose_url(url)
|
|
@@ -28,6 +49,10 @@ def load(
|
|
|
28
49
|
ln_setup.connect(instance)
|
|
29
50
|
import lamindb as ln
|
|
30
51
|
|
|
52
|
+
current_run = None
|
|
53
|
+
if get_current_run_file().exists():
|
|
54
|
+
current_run = ln.Run.get(uid=get_current_run_file().read_text().strip())
|
|
55
|
+
|
|
31
56
|
def script_to_notebook(
|
|
32
57
|
transform: ln.Transform, notebook_path: Path, bump_revision: bool = False
|
|
33
58
|
) -> None:
|
|
@@ -102,27 +127,25 @@ def load(
|
|
|
102
127
|
transforms = transforms.order_by("-created_at")
|
|
103
128
|
transform = transforms.first()
|
|
104
129
|
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if target_relpath.exists():
|
|
113
|
-
response = input(f"! {target_relpath} exists: replace? (y/n)")
|
|
130
|
+
target_path = Path(transform.key)
|
|
131
|
+
if ln_setup.settings.dev_dir is not None:
|
|
132
|
+
target_path = ln_setup.settings.dev_dir / target_path
|
|
133
|
+
if len(target_path.parents) > 1:
|
|
134
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
if target_path.exists():
|
|
136
|
+
response = input(f"! {target_path} exists: replace? (y/n)")
|
|
114
137
|
if response != "y":
|
|
115
138
|
raise SystemExit("Aborted.")
|
|
116
139
|
|
|
117
140
|
if transform.source_code is not None:
|
|
118
|
-
if
|
|
119
|
-
script_to_notebook(transform,
|
|
141
|
+
if target_path.suffix in (".ipynb", ".Rmd", ".qmd"):
|
|
142
|
+
script_to_notebook(transform, target_path, bump_revision=True)
|
|
120
143
|
else:
|
|
121
|
-
|
|
144
|
+
target_path.write_text(transform.source_code)
|
|
122
145
|
else:
|
|
123
146
|
raise SystemExit("No source code available for this transform.")
|
|
124
147
|
|
|
125
|
-
logger.important(f"{transform.type} is here: {
|
|
148
|
+
logger.important(f"{transform.type} is here: {target_path}")
|
|
126
149
|
|
|
127
150
|
if with_env:
|
|
128
151
|
ln.settings.track_run_inputs = False
|
|
@@ -132,8 +155,7 @@ def load(
|
|
|
132
155
|
):
|
|
133
156
|
filepath_env_cache = transform.latest_run.environment.cache()
|
|
134
157
|
target_env_filename = (
|
|
135
|
-
|
|
136
|
-
/ f"{target_relpath.stem}__requirements.txt"
|
|
158
|
+
target_path.parent / f"{target_path.stem}__requirements.txt"
|
|
137
159
|
)
|
|
138
160
|
shutil.move(filepath_env_cache, target_env_filename)
|
|
139
161
|
logger.important(f"environment is here: {target_env_filename}")
|
|
@@ -142,14 +164,13 @@ def load(
|
|
|
142
164
|
"latest transform run with environment doesn't exist"
|
|
143
165
|
)
|
|
144
166
|
|
|
145
|
-
return
|
|
167
|
+
return target_path
|
|
146
168
|
case "artifact" | "collection":
|
|
147
169
|
ln.settings.track_run_inputs = False
|
|
148
170
|
|
|
149
171
|
EntityClass = ln.Artifact if entity == "artifact" else ln.Collection
|
|
150
172
|
|
|
151
|
-
# we don't use .get here because DoesNotExist is hard to catch
|
|
152
|
-
# due to private django API
|
|
173
|
+
# we don't use .get here because DoesNotExist is hard to catch due to private django API
|
|
153
174
|
# we use `.objects` here because we don't want to exclude kind = __lamindb_run__ artifacts
|
|
154
175
|
if query_by_uid:
|
|
155
176
|
entities = EntityClass.objects.filter(uid__startswith=uid)
|
|
@@ -166,7 +187,7 @@ def load(
|
|
|
166
187
|
entities = entities.order_by("-created_at")
|
|
167
188
|
|
|
168
189
|
entity_obj = entities.first()
|
|
169
|
-
cache_path = entity_obj.cache()
|
|
190
|
+
cache_path = entity_obj.cache(is_run_input=current_run)
|
|
170
191
|
|
|
171
192
|
# collection gives us a list of paths
|
|
172
193
|
if isinstance(cache_path, list):
|