lamin_cli 1.11.0__py2.py3-none-any.whl → 1.12.1__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 +27 -27
- lamin_cli/__main__.py +826 -625
- lamin_cli/_annotate.py +47 -47
- lamin_cli/_cache.py +49 -41
- lamin_cli/_context.py +76 -76
- lamin_cli/_delete.py +85 -44
- lamin_cli/_io.py +147 -144
- lamin_cli/_load.py +203 -203
- lamin_cli/_migration.py +50 -50
- lamin_cli/_save.py +350 -325
- lamin_cli/_settings.py +154 -96
- lamin_cli/clone/_clone_verification.py +56 -56
- lamin_cli/clone/create_sqlite_clone_and_import_db.py +53 -51
- lamin_cli/compute/modal.py +174 -175
- lamin_cli/urls.py +10 -8
- {lamin_cli-1.11.0.dist-info → lamin_cli-1.12.1.dist-info}/METADATA +3 -2
- lamin_cli-1.12.1.dist-info/RECORD +22 -0
- {lamin_cli-1.11.0.dist-info → lamin_cli-1.12.1.dist-info}/WHEEL +1 -1
- {lamin_cli-1.11.0.dist-info → lamin_cli-1.12.1.dist-info/licenses}/LICENSE +201 -201
- lamin_cli-1.11.0.dist-info/RECORD +0 -22
- {lamin_cli-1.11.0.dist-info → lamin_cli-1.12.1.dist-info}/entry_points.txt +0 -0
lamin_cli/_annotate.py
CHANGED
|
@@ -1,47 +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
|
+
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/_cache.py
CHANGED
|
@@ -1,41 +1,49 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
if os.environ.get("NO_RICH"):
|
|
6
|
-
import click as click
|
|
7
|
-
else:
|
|
8
|
-
import rich_click as click
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@click.group()
|
|
12
|
-
def cache():
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@cache.command("set")
|
|
17
|
-
@click.argument(
|
|
18
|
-
"cache_dir",
|
|
19
|
-
type=click.Path(dir_okay=True, file_okay=False),
|
|
20
|
-
)
|
|
21
|
-
def set_cache(cache_dir: str):
|
|
22
|
-
"""Set the cache directory."""
|
|
23
|
-
from lamindb_setup._cache import set_cache_dir
|
|
24
|
-
|
|
25
|
-
set_cache_dir(cache_dir)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@cache.command("
|
|
29
|
-
def
|
|
30
|
-
"""
|
|
31
|
-
from lamindb_setup._cache import
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@cache.command("
|
|
37
|
-
def
|
|
38
|
-
"""
|
|
39
|
-
from lamindb_setup._cache import
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
if os.environ.get("NO_RICH"):
|
|
6
|
+
import click as click
|
|
7
|
+
else:
|
|
8
|
+
import rich_click as click
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def cache():
|
|
13
|
+
"""Get, set, reset, or clear the cache directory."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@cache.command("set")
|
|
17
|
+
@click.argument(
|
|
18
|
+
"cache_dir",
|
|
19
|
+
type=click.Path(dir_okay=True, file_okay=False),
|
|
20
|
+
)
|
|
21
|
+
def set_cache(cache_dir: str):
|
|
22
|
+
"""Set the path to the cache directory."""
|
|
23
|
+
from lamindb_setup._cache import set_cache_dir
|
|
24
|
+
|
|
25
|
+
set_cache_dir(cache_dir)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@cache.command("reset")
|
|
29
|
+
def reset_cache():
|
|
30
|
+
"""Reset the cache directory to the default path."""
|
|
31
|
+
from lamindb_setup._cache import set_cache_dir
|
|
32
|
+
|
|
33
|
+
set_cache_dir(None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@cache.command("clear")
|
|
37
|
+
def clear_cache():
|
|
38
|
+
"""Clear contents of the cache directory."""
|
|
39
|
+
from lamindb_setup._cache import clear_cache_dir
|
|
40
|
+
|
|
41
|
+
clear_cache_dir()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@cache.command("get")
|
|
45
|
+
def get_cache():
|
|
46
|
+
"""Get the path to the cache directory."""
|
|
47
|
+
from lamindb_setup._cache import get_cache_dir
|
|
48
|
+
|
|
49
|
+
click.echo(f"The cache directory is {get_cache_dir()}")
|
lamin_cli/_context.py
CHANGED
|
@@ -1,76 +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}")
|
|
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}")
|
lamin_cli/_delete.py
CHANGED
|
@@ -1,44 +1,85 @@
|
|
|
1
|
-
|
|
2
|
-
from lamindb_setup import
|
|
3
|
-
|
|
4
|
-
from .
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Branch
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
elif entity == "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
import click
|
|
2
|
+
from lamindb_setup import connect
|
|
3
|
+
from lamindb_setup import delete as delete_instance
|
|
4
|
+
from lamindb_setup.errors import StorageNotEmpty
|
|
5
|
+
|
|
6
|
+
from .urls import decompose_url
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def delete(
|
|
10
|
+
entity: str,
|
|
11
|
+
name: str | None = None,
|
|
12
|
+
uid: str | None = None,
|
|
13
|
+
key: str | None = None,
|
|
14
|
+
permanent: bool | None = None,
|
|
15
|
+
force: bool = False,
|
|
16
|
+
):
|
|
17
|
+
# TODO: refactor to abstract getting and deleting across entities
|
|
18
|
+
if entity.startswith("https://") and "lamin" in entity:
|
|
19
|
+
url = entity
|
|
20
|
+
instance, entity, uid = decompose_url(url)
|
|
21
|
+
connect(instance)
|
|
22
|
+
|
|
23
|
+
if entity == "branch":
|
|
24
|
+
assert name is not None, "You have to pass a name for deleting a branch."
|
|
25
|
+
from lamindb import Branch
|
|
26
|
+
|
|
27
|
+
Branch.get(name=name).delete(permanent=permanent)
|
|
28
|
+
elif entity == "artifact":
|
|
29
|
+
assert uid is not None or key is not None, (
|
|
30
|
+
"You have to pass a uid or key for deleting an artifact."
|
|
31
|
+
)
|
|
32
|
+
from lamindb import Artifact
|
|
33
|
+
|
|
34
|
+
if key is not None:
|
|
35
|
+
record = Artifact.objects.filter(key=key).order_by("-created_at").first()
|
|
36
|
+
if record is None:
|
|
37
|
+
raise SystemExit(f"Artifact with key={key} does not exist.")
|
|
38
|
+
else:
|
|
39
|
+
record = Artifact.get(uid)
|
|
40
|
+
record.delete(permanent=permanent)
|
|
41
|
+
elif entity == "transform":
|
|
42
|
+
assert uid is not None or key is not None, (
|
|
43
|
+
"You have to pass a uid or key for deleting a transform."
|
|
44
|
+
)
|
|
45
|
+
from lamindb import Transform
|
|
46
|
+
|
|
47
|
+
if key is not None:
|
|
48
|
+
record = Transform.objects.filter(key=key).order_by("-created_at").first()
|
|
49
|
+
if record is None:
|
|
50
|
+
raise SystemExit(f"Transform with key={key} does not exist.")
|
|
51
|
+
else:
|
|
52
|
+
record = Transform.get(uid)
|
|
53
|
+
record.delete(permanent=permanent)
|
|
54
|
+
elif entity == "collection":
|
|
55
|
+
assert uid is not None or key is not None, (
|
|
56
|
+
"You have to pass a uid or key for deleting a collection."
|
|
57
|
+
)
|
|
58
|
+
from lamindb import Collection
|
|
59
|
+
|
|
60
|
+
if key is not None:
|
|
61
|
+
record = Collection.objects.filter(key=key).order_by("-created_at").first()
|
|
62
|
+
if record is None:
|
|
63
|
+
raise SystemExit(f"Collection with key={key} does not exist.")
|
|
64
|
+
else:
|
|
65
|
+
record = Collection.get(uid)
|
|
66
|
+
record.delete(permanent=permanent)
|
|
67
|
+
elif entity == "record":
|
|
68
|
+
assert uid is not None or name is not None, (
|
|
69
|
+
"You have to pass a uid or name for deleting a record."
|
|
70
|
+
)
|
|
71
|
+
from lamindb import Record
|
|
72
|
+
|
|
73
|
+
if name is not None:
|
|
74
|
+
record = Record.objects.get(name=name)
|
|
75
|
+
if record is None:
|
|
76
|
+
raise SystemExit(f"Record with name={name} does not exist.")
|
|
77
|
+
else:
|
|
78
|
+
record = Record.get(uid)
|
|
79
|
+
record.delete(permanent=permanent)
|
|
80
|
+
else:
|
|
81
|
+
# could introduce "db" as an entity
|
|
82
|
+
try:
|
|
83
|
+
return delete_instance(entity, force=force)
|
|
84
|
+
except StorageNotEmpty as e:
|
|
85
|
+
raise click.ClickException(str(e)) from e
|