lamindb 0.76.8__py3-none-any.whl → 0.76.9__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.
- lamindb/__init__.py +113 -113
- lamindb/_artifact.py +1205 -1205
- lamindb/_can_validate.py +579 -579
- lamindb/_collection.py +389 -387
- lamindb/_curate.py +1601 -1601
- lamindb/_feature.py +155 -155
- lamindb/_feature_set.py +242 -242
- lamindb/_filter.py +23 -23
- lamindb/_finish.py +256 -256
- lamindb/_from_values.py +382 -382
- lamindb/_is_versioned.py +40 -40
- lamindb/_parents.py +476 -476
- lamindb/_query_manager.py +125 -125
- lamindb/_query_set.py +362 -362
- lamindb/_record.py +649 -649
- lamindb/_run.py +57 -57
- lamindb/_save.py +308 -308
- lamindb/_storage.py +14 -14
- lamindb/_transform.py +127 -127
- lamindb/_ulabel.py +56 -56
- lamindb/_utils.py +9 -9
- lamindb/_view.py +72 -72
- lamindb/core/__init__.py +94 -94
- lamindb/core/_context.py +574 -574
- lamindb/core/_data.py +438 -438
- lamindb/core/_feature_manager.py +867 -867
- lamindb/core/_label_manager.py +253 -253
- lamindb/core/_mapped_collection.py +631 -597
- lamindb/core/_settings.py +187 -187
- lamindb/core/_sync_git.py +138 -138
- lamindb/core/_track_environment.py +27 -27
- lamindb/core/datasets/__init__.py +59 -59
- lamindb/core/datasets/_core.py +581 -571
- lamindb/core/datasets/_fake.py +36 -36
- lamindb/core/exceptions.py +90 -90
- lamindb/core/fields.py +12 -12
- lamindb/core/loaders.py +164 -164
- lamindb/core/schema.py +56 -56
- lamindb/core/storage/__init__.py +25 -25
- lamindb/core/storage/_anndata_accessor.py +740 -740
- lamindb/core/storage/_anndata_sizes.py +41 -41
- lamindb/core/storage/_backed_access.py +98 -98
- lamindb/core/storage/_tiledbsoma.py +204 -204
- lamindb/core/storage/_valid_suffixes.py +21 -21
- lamindb/core/storage/_zarr.py +110 -110
- lamindb/core/storage/objects.py +62 -62
- lamindb/core/storage/paths.py +172 -172
- lamindb/core/subsettings/__init__.py +12 -12
- lamindb/core/subsettings/_creation_settings.py +38 -38
- lamindb/core/subsettings/_transform_settings.py +21 -21
- lamindb/core/types.py +19 -19
- lamindb/core/versioning.py +158 -158
- lamindb/integrations/__init__.py +12 -12
- lamindb/integrations/_vitessce.py +107 -107
- lamindb/setup/__init__.py +14 -14
- lamindb/setup/core/__init__.py +4 -4
- {lamindb-0.76.8.dist-info → lamindb-0.76.9.dist-info}/LICENSE +201 -201
- {lamindb-0.76.8.dist-info → lamindb-0.76.9.dist-info}/METADATA +4 -4
- lamindb-0.76.9.dist-info/RECORD +60 -0
- {lamindb-0.76.8.dist-info → lamindb-0.76.9.dist-info}/WHEEL +1 -1
- lamindb-0.76.8.dist-info/RECORD +0 -60
lamindb/core/versioning.py
CHANGED
@@ -1,158 +1,158 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from typing import TYPE_CHECKING, Literal
|
4
|
-
|
5
|
-
from lamin_utils import logger
|
6
|
-
from lamin_utils._base62 import CHARSET_DEFAULT as BASE62_CHARS
|
7
|
-
from lamindb_setup.core.upath import LocalPathClasses, UPath
|
8
|
-
from lnschema_core import ids
|
9
|
-
|
10
|
-
if TYPE_CHECKING:
|
11
|
-
from lnschema_core.models import IsVersioned
|
12
|
-
|
13
|
-
|
14
|
-
def message_update_key_in_version_family(
|
15
|
-
*,
|
16
|
-
suid: str,
|
17
|
-
existing_key: str,
|
18
|
-
registry: str,
|
19
|
-
new_key: str,
|
20
|
-
) -> str:
|
21
|
-
return f'Or update key "{existing_key}"
|
22
|
-
|
23
|
-
|
24
|
-
def increment_base62(s: str) -> str:
|
25
|
-
# we don't need to throw an error for zzzz because uids are enforced to be unique
|
26
|
-
# on the db level and have an enforced maximum length
|
27
|
-
value = sum(BASE62_CHARS.index(c) * (62**i) for i, c in enumerate(reversed(s)))
|
28
|
-
value += 1
|
29
|
-
result = ""
|
30
|
-
while value:
|
31
|
-
value, remainder = divmod(value, 62)
|
32
|
-
result = BASE62_CHARS[remainder] + result
|
33
|
-
return result.zfill(len(s))
|
34
|
-
|
35
|
-
|
36
|
-
def bump_version(
|
37
|
-
version: str,
|
38
|
-
bump_type: str = "minor",
|
39
|
-
behavior: Literal["prompt", "error", "ignore"] = "error",
|
40
|
-
) -> str:
|
41
|
-
"""Bumps the version number by major or minor depending on the bump_type flag.
|
42
|
-
|
43
|
-
Parameters:
|
44
|
-
version (str): The current version in "MAJOR" or "MAJOR.MINOR" format.
|
45
|
-
bump_type (str): The type of version bump, either 'major' or 'minor'.
|
46
|
-
|
47
|
-
Returns:
|
48
|
-
str: The new version string.
|
49
|
-
"""
|
50
|
-
try:
|
51
|
-
# Split the version into major and minor parts if possible
|
52
|
-
parts = version.split(".")
|
53
|
-
major = int(parts[0])
|
54
|
-
minor = int(parts[1]) if len(parts) > 1 else 0
|
55
|
-
|
56
|
-
if bump_type == "major":
|
57
|
-
# Bump the major version and reset the minor version
|
58
|
-
new_version = f"{major + 1}"
|
59
|
-
elif bump_type == "minor":
|
60
|
-
# Bump the minor version
|
61
|
-
new_version = f"{major}.{minor + 1}"
|
62
|
-
else:
|
63
|
-
raise ValueError("bump_type must be 'major' or 'minor'")
|
64
|
-
|
65
|
-
except (ValueError, IndexError):
|
66
|
-
if behavior == "prompt":
|
67
|
-
new_version = input(
|
68
|
-
f"The current version is '{version}' - please type the new version: "
|
69
|
-
)
|
70
|
-
elif behavior == "error":
|
71
|
-
raise ValueError(
|
72
|
-
"Cannot auto-increment non-integer castable version, please provide"
|
73
|
-
" manually"
|
74
|
-
) from None
|
75
|
-
else:
|
76
|
-
logger.warning("could not auto-increment version, fix '?' manually")
|
77
|
-
new_version = "?"
|
78
|
-
return new_version
|
79
|
-
|
80
|
-
|
81
|
-
def set_version(version: str | None = None, previous_version: str | None = None):
|
82
|
-
"""(Auto-) set version.
|
83
|
-
|
84
|
-
If `version` is `None`, returns the stored version.
|
85
|
-
Otherwise sets the version to the passed version.
|
86
|
-
|
87
|
-
Args:
|
88
|
-
version: Version string.
|
89
|
-
previous_version: Previous version string.
|
90
|
-
"""
|
91
|
-
if version is None and previous_version is not None:
|
92
|
-
version = bump_version(previous_version, bump_type="major")
|
93
|
-
return version
|
94
|
-
|
95
|
-
|
96
|
-
def create_uid(
|
97
|
-
*,
|
98
|
-
version: str | None = None,
|
99
|
-
n_full_id: int = 20,
|
100
|
-
revises: IsVersioned | None = None,
|
101
|
-
) -> tuple[str, IsVersioned | None]:
|
102
|
-
"""This also updates revises in case it's not the latest version.
|
103
|
-
|
104
|
-
This is why it returns revises.
|
105
|
-
"""
|
106
|
-
if revises is not None:
|
107
|
-
if not revises.is_latest:
|
108
|
-
# need one more request
|
109
|
-
revises = revises.__class__.objects.get(
|
110
|
-
is_latest=True, uid__startswith=revises.stem_uid
|
111
|
-
)
|
112
|
-
logger.warning(
|
113
|
-
f"didn't pass the latest version in `revises`, retrieved it: {revises}"
|
114
|
-
)
|
115
|
-
suid = revises.stem_uid
|
116
|
-
vuid = increment_base62(revises.uid[-4:])
|
117
|
-
else:
|
118
|
-
suid = ids.base62(n_full_id - 4)
|
119
|
-
vuid = "0000"
|
120
|
-
if version is not None:
|
121
|
-
if not isinstance(version, str):
|
122
|
-
raise ValueError(
|
123
|
-
"`version` parameter must be `None` or `str`, e.g., '0.1', '1', '2',"
|
124
|
-
" etc."
|
125
|
-
)
|
126
|
-
if revises is not None:
|
127
|
-
if version == revises.version:
|
128
|
-
raise ValueError(
|
129
|
-
f"Please increment the previous version: '{revises.version}'"
|
130
|
-
)
|
131
|
-
return suid + vuid, revises
|
132
|
-
|
133
|
-
|
134
|
-
def get_new_path_from_uid(old_path: UPath, old_uid: str, new_uid: str):
|
135
|
-
if isinstance(old_path, LocalPathClasses):
|
136
|
-
# for local path, the rename target must be full path
|
137
|
-
new_path = old_path.as_posix().replace(old_uid, new_uid)
|
138
|
-
else:
|
139
|
-
# for cloud path, the rename target must be the last part of the path
|
140
|
-
new_path = old_path.name.replace(old_uid, new_uid)
|
141
|
-
return new_path
|
142
|
-
|
143
|
-
|
144
|
-
def process_revises(
|
145
|
-
revises: IsVersioned | None,
|
146
|
-
version: str | None,
|
147
|
-
name: str | None,
|
148
|
-
type: type[IsVersioned],
|
149
|
-
) -> tuple[str, str, str, IsVersioned | None]:
|
150
|
-
if revises is not None and not isinstance(revises, type):
|
151
|
-
raise TypeError(f"`revises` has to be of type `{type.__name__}`")
|
152
|
-
uid, revises = create_uid(
|
153
|
-
revises=revises, version=version, n_full_id=type._len_full_uid
|
154
|
-
)
|
155
|
-
if revises is not None:
|
156
|
-
if name is None:
|
157
|
-
name = revises.name
|
158
|
-
return uid, version, name, revises
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING, Literal
|
4
|
+
|
5
|
+
from lamin_utils import logger
|
6
|
+
from lamin_utils._base62 import CHARSET_DEFAULT as BASE62_CHARS
|
7
|
+
from lamindb_setup.core.upath import LocalPathClasses, UPath
|
8
|
+
from lnschema_core import ids
|
9
|
+
|
10
|
+
if TYPE_CHECKING:
|
11
|
+
from lnschema_core.models import IsVersioned
|
12
|
+
|
13
|
+
|
14
|
+
def message_update_key_in_version_family(
|
15
|
+
*,
|
16
|
+
suid: str,
|
17
|
+
existing_key: str,
|
18
|
+
registry: str,
|
19
|
+
new_key: str,
|
20
|
+
) -> str:
|
21
|
+
return f'Or update key "{existing_key}" to "{new_key}":\n\nln.{registry}.filter(uid__startswith="{suid}").update(key="{new_key}")\n'
|
22
|
+
|
23
|
+
|
24
|
+
def increment_base62(s: str) -> str:
|
25
|
+
# we don't need to throw an error for zzzz because uids are enforced to be unique
|
26
|
+
# on the db level and have an enforced maximum length
|
27
|
+
value = sum(BASE62_CHARS.index(c) * (62**i) for i, c in enumerate(reversed(s)))
|
28
|
+
value += 1
|
29
|
+
result = ""
|
30
|
+
while value:
|
31
|
+
value, remainder = divmod(value, 62)
|
32
|
+
result = BASE62_CHARS[remainder] + result
|
33
|
+
return result.zfill(len(s))
|
34
|
+
|
35
|
+
|
36
|
+
def bump_version(
|
37
|
+
version: str,
|
38
|
+
bump_type: str = "minor",
|
39
|
+
behavior: Literal["prompt", "error", "ignore"] = "error",
|
40
|
+
) -> str:
|
41
|
+
"""Bumps the version number by major or minor depending on the bump_type flag.
|
42
|
+
|
43
|
+
Parameters:
|
44
|
+
version (str): The current version in "MAJOR" or "MAJOR.MINOR" format.
|
45
|
+
bump_type (str): The type of version bump, either 'major' or 'minor'.
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
str: The new version string.
|
49
|
+
"""
|
50
|
+
try:
|
51
|
+
# Split the version into major and minor parts if possible
|
52
|
+
parts = version.split(".")
|
53
|
+
major = int(parts[0])
|
54
|
+
minor = int(parts[1]) if len(parts) > 1 else 0
|
55
|
+
|
56
|
+
if bump_type == "major":
|
57
|
+
# Bump the major version and reset the minor version
|
58
|
+
new_version = f"{major + 1}"
|
59
|
+
elif bump_type == "minor":
|
60
|
+
# Bump the minor version
|
61
|
+
new_version = f"{major}.{minor + 1}"
|
62
|
+
else:
|
63
|
+
raise ValueError("bump_type must be 'major' or 'minor'")
|
64
|
+
|
65
|
+
except (ValueError, IndexError):
|
66
|
+
if behavior == "prompt":
|
67
|
+
new_version = input(
|
68
|
+
f"The current version is '{version}' - please type the new version: "
|
69
|
+
)
|
70
|
+
elif behavior == "error":
|
71
|
+
raise ValueError(
|
72
|
+
"Cannot auto-increment non-integer castable version, please provide"
|
73
|
+
" manually"
|
74
|
+
) from None
|
75
|
+
else:
|
76
|
+
logger.warning("could not auto-increment version, fix '?' manually")
|
77
|
+
new_version = "?"
|
78
|
+
return new_version
|
79
|
+
|
80
|
+
|
81
|
+
def set_version(version: str | None = None, previous_version: str | None = None):
|
82
|
+
"""(Auto-) set version.
|
83
|
+
|
84
|
+
If `version` is `None`, returns the stored version.
|
85
|
+
Otherwise sets the version to the passed version.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
version: Version string.
|
89
|
+
previous_version: Previous version string.
|
90
|
+
"""
|
91
|
+
if version is None and previous_version is not None:
|
92
|
+
version = bump_version(previous_version, bump_type="major")
|
93
|
+
return version
|
94
|
+
|
95
|
+
|
96
|
+
def create_uid(
|
97
|
+
*,
|
98
|
+
version: str | None = None,
|
99
|
+
n_full_id: int = 20,
|
100
|
+
revises: IsVersioned | None = None,
|
101
|
+
) -> tuple[str, IsVersioned | None]:
|
102
|
+
"""This also updates revises in case it's not the latest version.
|
103
|
+
|
104
|
+
This is why it returns revises.
|
105
|
+
"""
|
106
|
+
if revises is not None:
|
107
|
+
if not revises.is_latest:
|
108
|
+
# need one more request
|
109
|
+
revises = revises.__class__.objects.get(
|
110
|
+
is_latest=True, uid__startswith=revises.stem_uid
|
111
|
+
)
|
112
|
+
logger.warning(
|
113
|
+
f"didn't pass the latest version in `revises`, retrieved it: {revises}"
|
114
|
+
)
|
115
|
+
suid = revises.stem_uid
|
116
|
+
vuid = increment_base62(revises.uid[-4:])
|
117
|
+
else:
|
118
|
+
suid = ids.base62(n_full_id - 4)
|
119
|
+
vuid = "0000"
|
120
|
+
if version is not None:
|
121
|
+
if not isinstance(version, str):
|
122
|
+
raise ValueError(
|
123
|
+
"`version` parameter must be `None` or `str`, e.g., '0.1', '1', '2',"
|
124
|
+
" etc."
|
125
|
+
)
|
126
|
+
if revises is not None:
|
127
|
+
if version == revises.version:
|
128
|
+
raise ValueError(
|
129
|
+
f"Please increment the previous version: '{revises.version}'"
|
130
|
+
)
|
131
|
+
return suid + vuid, revises
|
132
|
+
|
133
|
+
|
134
|
+
def get_new_path_from_uid(old_path: UPath, old_uid: str, new_uid: str):
|
135
|
+
if isinstance(old_path, LocalPathClasses):
|
136
|
+
# for local path, the rename target must be full path
|
137
|
+
new_path = old_path.as_posix().replace(old_uid, new_uid)
|
138
|
+
else:
|
139
|
+
# for cloud path, the rename target must be the last part of the path
|
140
|
+
new_path = old_path.name.replace(old_uid, new_uid)
|
141
|
+
return new_path
|
142
|
+
|
143
|
+
|
144
|
+
def process_revises(
|
145
|
+
revises: IsVersioned | None,
|
146
|
+
version: str | None,
|
147
|
+
name: str | None,
|
148
|
+
type: type[IsVersioned],
|
149
|
+
) -> tuple[str, str, str, IsVersioned | None]:
|
150
|
+
if revises is not None and not isinstance(revises, type):
|
151
|
+
raise TypeError(f"`revises` has to be of type `{type.__name__}`")
|
152
|
+
uid, revises = create_uid(
|
153
|
+
revises=revises, version=version, n_full_id=type._len_full_uid
|
154
|
+
)
|
155
|
+
if revises is not None:
|
156
|
+
if name is None:
|
157
|
+
name = revises.name
|
158
|
+
return uid, version, name, revises
|
lamindb/integrations/__init__.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
"""Integrations.
|
2
|
-
|
3
|
-
.. autosummary::
|
4
|
-
:toctree: .
|
5
|
-
|
6
|
-
save_vitessce_config
|
7
|
-
save_tiledbsoma_experiment
|
8
|
-
"""
|
9
|
-
|
10
|
-
from lamindb.core.storage import save_tiledbsoma_experiment
|
11
|
-
|
12
|
-
from ._vitessce import save_vitessce_config
|
1
|
+
"""Integrations.
|
2
|
+
|
3
|
+
.. autosummary::
|
4
|
+
:toctree: .
|
5
|
+
|
6
|
+
save_vitessce_config
|
7
|
+
save_tiledbsoma_experiment
|
8
|
+
"""
|
9
|
+
|
10
|
+
from lamindb.core.storage import save_tiledbsoma_experiment
|
11
|
+
|
12
|
+
from ._vitessce import save_vitessce_config
|
@@ -1,107 +1,107 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import json
|
4
|
-
from datetime import datetime, timezone
|
5
|
-
from typing import TYPE_CHECKING
|
6
|
-
|
7
|
-
import lamindb_setup as ln_setup
|
8
|
-
from lamin_utils import logger
|
9
|
-
|
10
|
-
from lamindb._artifact import Artifact
|
11
|
-
from lamindb._run import Run
|
12
|
-
from lamindb._transform import Transform
|
13
|
-
|
14
|
-
if TYPE_CHECKING:
|
15
|
-
from vitessce import VitessceConfig
|
16
|
-
|
17
|
-
|
18
|
-
# "unit test": https://github.com/laminlabs/lamindb/blob/main/docs/storage/vitessce.ipynb
|
19
|
-
# integration test & context: https://github.com/laminlabs/lamin-spatial/blob/main/docs/vitessce.ipynb
|
20
|
-
def save_vitessce_config(
|
21
|
-
vitessce_config: VitessceConfig, description: str | None = None
|
22
|
-
) -> Artifact:
|
23
|
-
"""Validates and saves a ``VitessceConfig`` object.
|
24
|
-
|
25
|
-
Guide: :doc:`docs:vitessce`.
|
26
|
-
|
27
|
-
Args:
|
28
|
-
vitessce_config (``VitessceConfig``): A `VitessceConfig` object.
|
29
|
-
description: A description for the `VitessceConfig` artifact.
|
30
|
-
|
31
|
-
.. versionchanged:: 0.75.1
|
32
|
-
Now displays the "Vitessce button" on the hub next to the dataset. It additionally keeps displaying it next to the configuration file.
|
33
|
-
.. versionchanged:: 0.70.2
|
34
|
-
No longer saves the dataset. It only saves the `VitessceConfig` object.
|
35
|
-
"""
|
36
|
-
# can only import here because vitessce is not a dependency
|
37
|
-
from vitessce import VitessceConfig
|
38
|
-
|
39
|
-
from lamindb.core.storage import VALID_SUFFIXES
|
40
|
-
|
41
|
-
assert isinstance(vitessce_config, VitessceConfig) # noqa: S101
|
42
|
-
vc_dict = vitessce_config.to_dict()
|
43
|
-
valid_composite_zarr_suffixes = [
|
44
|
-
suffix for suffix in VALID_SUFFIXES.COMPOSITE if suffix.endswith(".zarr")
|
45
|
-
]
|
46
|
-
# validate
|
47
|
-
dataset_artifacts = []
|
48
|
-
assert vc_dict["datasets"] # noqa: S101
|
49
|
-
for vitessce_dataset in vc_dict["datasets"]:
|
50
|
-
# didn't find good ways to violate the below, hence using plain asserts
|
51
|
-
# without user feedback
|
52
|
-
assert "files" in vitessce_dataset # noqa: S101
|
53
|
-
assert vitessce_dataset["files"] # noqa: S101
|
54
|
-
for file in vitessce_dataset["files"]:
|
55
|
-
if "url" not in file:
|
56
|
-
raise ValueError("Each file must have a 'url' key.")
|
57
|
-
s3_path = file["url"]
|
58
|
-
s3_path_last_element = s3_path.split("/")[-1]
|
59
|
-
# now start with attempting to strip the composite suffix candidates
|
60
|
-
for suffix in valid_composite_zarr_suffixes:
|
61
|
-
s3_path_last_element = s3_path_last_element.replace(suffix, "")
|
62
|
-
# in case there was no hit, strip plain ".zarr"
|
63
|
-
artifact_stem_uid = s3_path_last_element.replace(".zarr", "")
|
64
|
-
# if there is still a "." in string, raise an error
|
65
|
-
if "." in artifact_stem_uid:
|
66
|
-
raise ValueError(
|
67
|
-
f"Suffix should be '.zarr' or one of {valid_composite_zarr_suffixes}. Inspect your path {s3_path}"
|
68
|
-
)
|
69
|
-
artifact = Artifact.filter(uid__startswith=artifact_stem_uid).one_or_none()
|
70
|
-
if artifact is None:
|
71
|
-
raise ValueError(
|
72
|
-
f"Could not find dataset with stem uid '{artifact_stem_uid}' in lamindb: {vitessce_dataset}. Did you follow https://docs.lamin.ai/vitessce? It appears the AWS S3 path doesn't encode a lamindb uid."
|
73
|
-
)
|
74
|
-
else:
|
75
|
-
dataset_artifacts.append(artifact)
|
76
|
-
# the below will be replaced with a `ln.tracked()` decorator soon
|
77
|
-
with logger.mute():
|
78
|
-
transform = Transform(
|
79
|
-
uid="kup03MJBsIVa0001",
|
80
|
-
name="save_vitessce_config",
|
81
|
-
type="function",
|
82
|
-
version="2",
|
83
|
-
).save()
|
84
|
-
run = Run(transform=transform).save()
|
85
|
-
if len(dataset_artifacts) > 1:
|
86
|
-
# if we have more datasets, we should create a collection
|
87
|
-
# and attach an action to the collection
|
88
|
-
raise NotImplementedError
|
89
|
-
run.input_artifacts.set(dataset_artifacts)
|
90
|
-
# create a JSON export
|
91
|
-
config_file_local_path = (
|
92
|
-
ln_setup.settings.storage.cache_dir / "config.vitessce.json"
|
93
|
-
)
|
94
|
-
with open(config_file_local_path, "w") as file:
|
95
|
-
json.dump(vc_dict, file)
|
96
|
-
vitessce_config_artifact = Artifact(
|
97
|
-
config_file_local_path, description=description, run=run
|
98
|
-
).save()
|
99
|
-
# we have one and only one dataset artifact, hence the following line is OK
|
100
|
-
dataset_artifacts[0]._actions.add(vitessce_config_artifact)
|
101
|
-
slug = ln_setup.settings.instance.slug
|
102
|
-
logger.important(
|
103
|
-
f"go to: https://lamin.ai/{slug}/artifact/{vitessce_config_artifact.uid}"
|
104
|
-
)
|
105
|
-
run.finished_at = datetime.now(timezone.utc)
|
106
|
-
run.save()
|
107
|
-
return vitessce_config_artifact
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import json
|
4
|
+
from datetime import datetime, timezone
|
5
|
+
from typing import TYPE_CHECKING
|
6
|
+
|
7
|
+
import lamindb_setup as ln_setup
|
8
|
+
from lamin_utils import logger
|
9
|
+
|
10
|
+
from lamindb._artifact import Artifact
|
11
|
+
from lamindb._run import Run
|
12
|
+
from lamindb._transform import Transform
|
13
|
+
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from vitessce import VitessceConfig
|
16
|
+
|
17
|
+
|
18
|
+
# "unit test": https://github.com/laminlabs/lamindb/blob/main/docs/storage/vitessce.ipynb
|
19
|
+
# integration test & context: https://github.com/laminlabs/lamin-spatial/blob/main/docs/vitessce.ipynb
|
20
|
+
def save_vitessce_config(
|
21
|
+
vitessce_config: VitessceConfig, description: str | None = None
|
22
|
+
) -> Artifact:
|
23
|
+
"""Validates and saves a ``VitessceConfig`` object.
|
24
|
+
|
25
|
+
Guide: :doc:`docs:vitessce`.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
vitessce_config (``VitessceConfig``): A `VitessceConfig` object.
|
29
|
+
description: A description for the `VitessceConfig` artifact.
|
30
|
+
|
31
|
+
.. versionchanged:: 0.75.1
|
32
|
+
Now displays the "Vitessce button" on the hub next to the dataset. It additionally keeps displaying it next to the configuration file.
|
33
|
+
.. versionchanged:: 0.70.2
|
34
|
+
No longer saves the dataset. It only saves the `VitessceConfig` object.
|
35
|
+
"""
|
36
|
+
# can only import here because vitessce is not a dependency
|
37
|
+
from vitessce import VitessceConfig
|
38
|
+
|
39
|
+
from lamindb.core.storage import VALID_SUFFIXES
|
40
|
+
|
41
|
+
assert isinstance(vitessce_config, VitessceConfig) # noqa: S101
|
42
|
+
vc_dict = vitessce_config.to_dict()
|
43
|
+
valid_composite_zarr_suffixes = [
|
44
|
+
suffix for suffix in VALID_SUFFIXES.COMPOSITE if suffix.endswith(".zarr")
|
45
|
+
]
|
46
|
+
# validate
|
47
|
+
dataset_artifacts = []
|
48
|
+
assert vc_dict["datasets"] # noqa: S101
|
49
|
+
for vitessce_dataset in vc_dict["datasets"]:
|
50
|
+
# didn't find good ways to violate the below, hence using plain asserts
|
51
|
+
# without user feedback
|
52
|
+
assert "files" in vitessce_dataset # noqa: S101
|
53
|
+
assert vitessce_dataset["files"] # noqa: S101
|
54
|
+
for file in vitessce_dataset["files"]:
|
55
|
+
if "url" not in file:
|
56
|
+
raise ValueError("Each file must have a 'url' key.")
|
57
|
+
s3_path = file["url"]
|
58
|
+
s3_path_last_element = s3_path.split("/")[-1]
|
59
|
+
# now start with attempting to strip the composite suffix candidates
|
60
|
+
for suffix in valid_composite_zarr_suffixes:
|
61
|
+
s3_path_last_element = s3_path_last_element.replace(suffix, "")
|
62
|
+
# in case there was no hit, strip plain ".zarr"
|
63
|
+
artifact_stem_uid = s3_path_last_element.replace(".zarr", "")
|
64
|
+
# if there is still a "." in string, raise an error
|
65
|
+
if "." in artifact_stem_uid:
|
66
|
+
raise ValueError(
|
67
|
+
f"Suffix should be '.zarr' or one of {valid_composite_zarr_suffixes}. Inspect your path {s3_path}"
|
68
|
+
)
|
69
|
+
artifact = Artifact.filter(uid__startswith=artifact_stem_uid).one_or_none()
|
70
|
+
if artifact is None:
|
71
|
+
raise ValueError(
|
72
|
+
f"Could not find dataset with stem uid '{artifact_stem_uid}' in lamindb: {vitessce_dataset}. Did you follow https://docs.lamin.ai/vitessce? It appears the AWS S3 path doesn't encode a lamindb uid."
|
73
|
+
)
|
74
|
+
else:
|
75
|
+
dataset_artifacts.append(artifact)
|
76
|
+
# the below will be replaced with a `ln.tracked()` decorator soon
|
77
|
+
with logger.mute():
|
78
|
+
transform = Transform(
|
79
|
+
uid="kup03MJBsIVa0001",
|
80
|
+
name="save_vitessce_config",
|
81
|
+
type="function",
|
82
|
+
version="2",
|
83
|
+
).save()
|
84
|
+
run = Run(transform=transform).save()
|
85
|
+
if len(dataset_artifacts) > 1:
|
86
|
+
# if we have more datasets, we should create a collection
|
87
|
+
# and attach an action to the collection
|
88
|
+
raise NotImplementedError
|
89
|
+
run.input_artifacts.set(dataset_artifacts)
|
90
|
+
# create a JSON export
|
91
|
+
config_file_local_path = (
|
92
|
+
ln_setup.settings.storage.cache_dir / "config.vitessce.json"
|
93
|
+
)
|
94
|
+
with open(config_file_local_path, "w") as file:
|
95
|
+
json.dump(vc_dict, file)
|
96
|
+
vitessce_config_artifact = Artifact(
|
97
|
+
config_file_local_path, description=description, run=run
|
98
|
+
).save()
|
99
|
+
# we have one and only one dataset artifact, hence the following line is OK
|
100
|
+
dataset_artifacts[0]._actions.add(vitessce_config_artifact)
|
101
|
+
slug = ln_setup.settings.instance.slug
|
102
|
+
logger.important(
|
103
|
+
f"go to: https://lamin.ai/{slug}/artifact/{vitessce_config_artifact.uid}"
|
104
|
+
)
|
105
|
+
run.finished_at = datetime.now(timezone.utc)
|
106
|
+
run.save()
|
107
|
+
return vitessce_config_artifact
|
lamindb/setup/__init__.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
|
-
import lamindb_setup as _lamindb_setup
|
2
|
-
from lamindb_setup import * # noqa: F403
|
3
|
-
from lamindb_setup import (
|
4
|
-
connect,
|
5
|
-
delete,
|
6
|
-
init,
|
7
|
-
settings,
|
8
|
-
)
|
9
|
-
|
10
|
-
from . import core
|
11
|
-
|
12
|
-
del connect # we have this at the root level, hence, we don't want it here
|
13
|
-
__doc__ = _lamindb_setup.__doc__.replace("lamindb_setup", "lamindb.setup")
|
14
|
-
settings.__doc__ = settings.__doc__.replace("lamindb_setup", "lamindb.setup")
|
1
|
+
import lamindb_setup as _lamindb_setup
|
2
|
+
from lamindb_setup import * # noqa: F403
|
3
|
+
from lamindb_setup import (
|
4
|
+
connect,
|
5
|
+
delete,
|
6
|
+
init,
|
7
|
+
settings,
|
8
|
+
)
|
9
|
+
|
10
|
+
from . import core
|
11
|
+
|
12
|
+
del connect # we have this at the root level, hence, we don't want it here
|
13
|
+
__doc__ = _lamindb_setup.__doc__.replace("lamindb_setup", "lamindb.setup")
|
14
|
+
settings.__doc__ = settings.__doc__.replace("lamindb_setup", "lamindb.setup")
|
lamindb/setup/core/__init__.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import lamindb_setup as _lamindb_setup
|
2
|
-
from lamindb_setup.core import * # noqa: F403
|
3
|
-
|
4
|
-
__doc__ = _lamindb_setup.core.__doc__.replace("lamindb_setup", "lamindb.setup")
|
1
|
+
import lamindb_setup as _lamindb_setup
|
2
|
+
from lamindb_setup.core import * # noqa: F403
|
3
|
+
|
4
|
+
__doc__ = _lamindb_setup.core.__doc__.replace("lamindb_setup", "lamindb.setup")
|