lamindb 1.0.3__py3-none-any.whl → 1.0.5__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 +1 -1
- lamindb/_artifact.py +24 -4
- lamindb/_finish.py +213 -78
- lamindb/core/_context.py +32 -29
- lamindb/core/_feature_manager.py +1 -1
- lamindb/core/loaders.py +1 -1
- lamindb/core/storage/paths.py +14 -1
- lamindb/migrations/0081_revert_textfield_collection.py +21 -0
- lamindb/models.py +7 -4
- {lamindb-1.0.3.dist-info → lamindb-1.0.5.dist-info}/METADATA +3 -2
- {lamindb-1.0.3.dist-info → lamindb-1.0.5.dist-info}/RECORD +13 -12
- {lamindb-1.0.3.dist-info → lamindb-1.0.5.dist-info}/LICENSE +0 -0
- {lamindb-1.0.3.dist-info → lamindb-1.0.5.dist-info}/WHEEL +0 -0
lamindb/__init__.py
CHANGED
@@ -53,7 +53,7 @@ Modules and settings.
|
|
53
53
|
"""
|
54
54
|
|
55
55
|
# denote a release candidate for 0.1.0 with 0.1rc1, 0.1a1, 0.1b1, etc.
|
56
|
-
__version__ = "1.0.
|
56
|
+
__version__ = "1.0.5"
|
57
57
|
|
58
58
|
from lamindb_setup._check_setup import InstanceNotSetupError as _InstanceNotSetupError
|
59
59
|
from lamindb_setup._check_setup import _check_instance_setup
|
lamindb/_artifact.py
CHANGED
@@ -64,7 +64,7 @@ try:
|
|
64
64
|
except ImportError:
|
65
65
|
|
66
66
|
def zarr_is_adata(storepath): # type: ignore
|
67
|
-
raise ImportError("Please install zarr: pip install zarr")
|
67
|
+
raise ImportError("Please install zarr: pip install zarr<=2.18.4")
|
68
68
|
|
69
69
|
|
70
70
|
if TYPE_CHECKING:
|
@@ -845,7 +845,7 @@ def from_dir(
|
|
845
845
|
# docstring handled through attach_func_to_class_method
|
846
846
|
def replace(
|
847
847
|
self,
|
848
|
-
data: UPathStr,
|
848
|
+
data: UPathStr | pd.DataFrame | AnnData | MuData,
|
849
849
|
run: Run | None = None,
|
850
850
|
format: str | None = None,
|
851
851
|
) -> None:
|
@@ -867,7 +867,18 @@ def replace(
|
|
867
867
|
|
868
868
|
check_path_in_storage = privates["check_path_in_storage"]
|
869
869
|
if check_path_in_storage:
|
870
|
-
|
870
|
+
err_msg = (
|
871
|
+
"Can only replace with a local path not in any Storage. "
|
872
|
+
f"This data is in {Storage.objects.get(id=kwargs['storage_id'])}."
|
873
|
+
)
|
874
|
+
raise ValueError(err_msg)
|
875
|
+
|
876
|
+
_overwrite_versions = kwargs["_overwrite_versions"]
|
877
|
+
if self._overwrite_versions != _overwrite_versions:
|
878
|
+
err_msg = "It is not allowed to replace "
|
879
|
+
err_msg += "a folder" if self._overwrite_versions else "a file"
|
880
|
+
err_msg += " with " + ("a folder." if _overwrite_versions else "a file.")
|
881
|
+
raise ValueError(err_msg)
|
871
882
|
|
872
883
|
if self.key is not None and not self._key_is_virtual:
|
873
884
|
key_path = PurePosixPath(self.key)
|
@@ -902,6 +913,7 @@ def replace(
|
|
902
913
|
self._hash_type = kwargs["_hash_type"]
|
903
914
|
self.run_id = kwargs["run_id"]
|
904
915
|
self.run = kwargs["run"]
|
916
|
+
self.n_files = kwargs["n_files"]
|
905
917
|
|
906
918
|
self._local_filepath = privates["local_filepath"]
|
907
919
|
self._cloud_filepath = privates["cloud_filepath"]
|
@@ -926,7 +938,15 @@ def open(
|
|
926
938
|
if self._overwrite_versions and not self.is_latest:
|
927
939
|
raise ValueError(inconsistent_state_msg)
|
928
940
|
# ignore empty suffix for now
|
929
|
-
suffixes = (
|
941
|
+
suffixes = (
|
942
|
+
"",
|
943
|
+
".h5",
|
944
|
+
".hdf5",
|
945
|
+
".h5ad",
|
946
|
+
".zarr",
|
947
|
+
".anndata.zarr",
|
948
|
+
".tiledbsoma",
|
949
|
+
) + PYARROW_SUFFIXES
|
930
950
|
if self.suffix not in suffixes:
|
931
951
|
raise ValueError(
|
932
952
|
"Artifact should have a zarr, h5, tiledbsoma object"
|
lamindb/_finish.py
CHANGED
@@ -1,22 +1,84 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import builtins
|
3
4
|
import re
|
4
5
|
from datetime import datetime, timezone
|
6
|
+
from time import sleep
|
5
7
|
from typing import TYPE_CHECKING
|
6
8
|
|
7
9
|
import lamindb_setup as ln_setup
|
8
10
|
from lamin_utils import logger
|
11
|
+
from lamin_utils._logger import LEVEL_TO_COLORS, LEVEL_TO_ICONS, RESET_COLOR
|
9
12
|
from lamindb_setup.core.hashing import hash_file
|
10
13
|
|
11
|
-
from lamindb.core.exceptions import NotebookNotSaved
|
12
14
|
from lamindb.models import Artifact, Run, Transform
|
13
15
|
|
16
|
+
is_run_from_ipython = getattr(builtins, "__IPYTHON__", False)
|
17
|
+
|
14
18
|
if TYPE_CHECKING:
|
15
19
|
from pathlib import Path
|
16
20
|
|
17
21
|
|
18
22
|
def get_save_notebook_message() -> str:
|
19
|
-
|
23
|
+
# do not add bold() or any other complicated characters as then we can't match this
|
24
|
+
# easily anymore in an html to strip it out
|
25
|
+
return f"please hit {get_shortcut()} to save the notebook in your editor"
|
26
|
+
|
27
|
+
|
28
|
+
def get_save_notebook_message_retry() -> str:
|
29
|
+
return f"{get_save_notebook_message()} and re-run finish()"
|
30
|
+
|
31
|
+
|
32
|
+
# this code was originally in nbproject by the same authors
|
33
|
+
def check_consecutiveness(
|
34
|
+
nb, calling_statement: str = None, silent_success: bool = True
|
35
|
+
) -> bool:
|
36
|
+
"""Check whether code cells have been executed consecutively.
|
37
|
+
|
38
|
+
Needs to be called in the last code cell of a notebook.
|
39
|
+
Otherwise raises `RuntimeError`.
|
40
|
+
|
41
|
+
Returns cell transitions that violate execution at increments of 1 as a list
|
42
|
+
of tuples.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
nb: Notebook content.
|
46
|
+
calling_statement: The statement that calls this function.
|
47
|
+
"""
|
48
|
+
cells = nb.cells
|
49
|
+
|
50
|
+
violations = []
|
51
|
+
prev = 0
|
52
|
+
|
53
|
+
ccount = 0 # need to initialize because notebook might note have code cells
|
54
|
+
# and below, we check if ccount is None
|
55
|
+
for cell in cells:
|
56
|
+
cell_source = "".join(cell["source"])
|
57
|
+
if cell["cell_type"] != "code" or cell_source == "":
|
58
|
+
continue
|
59
|
+
|
60
|
+
if calling_statement is not None and calling_statement in cell_source:
|
61
|
+
continue
|
62
|
+
|
63
|
+
ccount = cell["execution_count"]
|
64
|
+
if ccount is None or prev is None or ccount - prev != 1:
|
65
|
+
violations.append((prev, ccount))
|
66
|
+
|
67
|
+
prev = ccount
|
68
|
+
|
69
|
+
# ignore the very last code cell of the notebook
|
70
|
+
# `check_consecutiveness` is being run during publish if `last_cell`` is True
|
71
|
+
# hence, that cell has ccount is None
|
72
|
+
if ccount is None:
|
73
|
+
violations.pop()
|
74
|
+
|
75
|
+
any_violations = len(violations) > 0
|
76
|
+
if any_violations:
|
77
|
+
logger.warning(f"cells {violations} were not run consecutively")
|
78
|
+
elif not silent_success:
|
79
|
+
logger.success("cell execution numbers increase consecutively")
|
80
|
+
|
81
|
+
return not any_violations
|
20
82
|
|
21
83
|
|
22
84
|
def get_shortcut() -> str:
|
@@ -64,11 +126,11 @@ def prepare_notebook(
|
|
64
126
|
if strip_title:
|
65
127
|
lines.pop(i)
|
66
128
|
cell["source"] = "\n".join(lines)
|
67
|
-
# strip
|
129
|
+
# strip logging message about saving notebook in editor
|
68
130
|
# this is normally the last cell
|
69
131
|
if cell["cell_type"] == "code" and ".finish(" in cell["source"]:
|
70
132
|
for output in cell["outputs"]:
|
71
|
-
if output.get("
|
133
|
+
if "to save the notebook in your editor" in output.get("text", ""):
|
72
134
|
cell["outputs"] = []
|
73
135
|
break
|
74
136
|
return None
|
@@ -114,7 +176,6 @@ def notebook_to_script(
|
|
114
176
|
script_path.write_text(py_content)
|
115
177
|
|
116
178
|
|
117
|
-
# removes NotebookNotSaved error message from notebook html
|
118
179
|
def clean_r_notebook_html(file_path: Path) -> tuple[str | None, Path]:
|
119
180
|
import re
|
120
181
|
|
@@ -129,15 +190,15 @@ def clean_r_notebook_html(file_path: Path) -> tuple[str | None, Path]:
|
|
129
190
|
cleaned_content = re.sub(pattern_title, "", cleaned_content)
|
130
191
|
cleaned_content = re.sub(pattern_h1, "", cleaned_content)
|
131
192
|
# remove error message from content
|
132
|
-
if "
|
133
|
-
orig_error_message = f"
|
193
|
+
if "to save the notebook in your editor" in cleaned_content:
|
194
|
+
orig_error_message = f"! {get_save_notebook_message_retry()}"
|
134
195
|
# coming up with the regex for this is a bit tricky due to all the
|
135
196
|
# escape characters we'd need to insert into the message; hence,
|
136
197
|
# we do this with a replace() instead
|
137
198
|
cleaned_content = cleaned_content.replace(orig_error_message, "")
|
138
|
-
if "
|
199
|
+
if "to save the notebook in your editor" in cleaned_content:
|
139
200
|
orig_error_message = orig_error_message.replace(
|
140
|
-
"
|
201
|
+
" finish()", "\nfinish()"
|
141
202
|
) # RStudio might insert a newline
|
142
203
|
cleaned_content = cleaned_content.replace(orig_error_message, "")
|
143
204
|
cleaned_path = file_path.parent / (f"{file_path.stem}.cleaned{file_path.suffix}")
|
@@ -145,6 +206,30 @@ def clean_r_notebook_html(file_path: Path) -> tuple[str | None, Path]:
|
|
145
206
|
return title_text, cleaned_path
|
146
207
|
|
147
208
|
|
209
|
+
def check_filepath_recently_saved(filepath: Path, is_finish_retry: bool) -> bool:
|
210
|
+
recently_saved_time = 3 if not is_finish_retry else 20
|
211
|
+
for retry in range(30):
|
212
|
+
if get_seconds_since_modified(filepath) > recently_saved_time:
|
213
|
+
if retry == 0:
|
214
|
+
prefix = f"{LEVEL_TO_COLORS[20]}{LEVEL_TO_ICONS[20]}{RESET_COLOR}"
|
215
|
+
print(f"{prefix} {get_save_notebook_message()}", end=" ")
|
216
|
+
elif retry == 9:
|
217
|
+
print(".", end="\n")
|
218
|
+
elif retry == 4:
|
219
|
+
print(". still waiting ", end="")
|
220
|
+
else:
|
221
|
+
print(".", end="")
|
222
|
+
sleep(1)
|
223
|
+
else:
|
224
|
+
if retry > 0:
|
225
|
+
prefix = f"{LEVEL_TO_COLORS[25]}{LEVEL_TO_ICONS[25]}{RESET_COLOR}"
|
226
|
+
print(f" {prefix}")
|
227
|
+
# filepath was recently saved, return True
|
228
|
+
return True
|
229
|
+
# if we arrive here, no save event occured, return False
|
230
|
+
return False
|
231
|
+
|
232
|
+
|
148
233
|
def save_context_core(
|
149
234
|
*,
|
150
235
|
run: Run | None,
|
@@ -153,6 +238,7 @@ def save_context_core(
|
|
153
238
|
finished_at: bool = False,
|
154
239
|
ignore_non_consecutive: bool | None = None,
|
155
240
|
from_cli: bool = False,
|
241
|
+
is_retry: bool = False,
|
156
242
|
) -> str | None:
|
157
243
|
import lamindb as ln
|
158
244
|
from lamindb.models import (
|
@@ -167,12 +253,29 @@ def save_context_core(
|
|
167
253
|
is_r_notebook = filepath.suffix in {".qmd", ".Rmd"}
|
168
254
|
source_code_path = filepath
|
169
255
|
report_path: Path | None = None
|
170
|
-
|
171
|
-
if
|
256
|
+
save_source_code_and_report = True
|
257
|
+
if is_run_from_ipython: # python notebooks in interactive session
|
258
|
+
import nbproject
|
259
|
+
|
260
|
+
# it might be that the user modifies the title just before ln.finish()
|
261
|
+
if (nbproject_title := nbproject.meta.live.title) != transform.description:
|
262
|
+
transform.description = nbproject_title
|
263
|
+
transform.save()
|
264
|
+
if not ln_setup._TESTING:
|
265
|
+
save_source_code_and_report = check_filepath_recently_saved(
|
266
|
+
filepath, is_retry
|
267
|
+
)
|
268
|
+
if not save_source_code_and_report and not is_retry:
|
269
|
+
logger.warning(get_save_notebook_message_retry())
|
270
|
+
return "retry"
|
271
|
+
elif not save_source_code_and_report:
|
272
|
+
logger.warning(
|
273
|
+
"the notebook on disk wasn't saved within the last 10 sec"
|
274
|
+
)
|
275
|
+
if is_ipynb: # could be from CLI outside interactive session
|
172
276
|
try:
|
173
277
|
import jupytext # noqa: F401
|
174
278
|
from nbproject.dev import (
|
175
|
-
check_consecutiveness,
|
176
279
|
read_notebook,
|
177
280
|
)
|
178
281
|
except ImportError:
|
@@ -185,7 +288,9 @@ def save_context_core(
|
|
185
288
|
)
|
186
289
|
if not is_consecutive:
|
187
290
|
response = "n" # ignore_non_consecutive == False
|
188
|
-
if ignore_non_consecutive is None:
|
291
|
+
if ignore_non_consecutive is None: # only print warning
|
292
|
+
response = "y" # we already printed the warning
|
293
|
+
else: # ask user to confirm
|
189
294
|
response = input(
|
190
295
|
" Do you still want to proceed with finishing? (y/n) "
|
191
296
|
)
|
@@ -210,34 +315,44 @@ def save_context_core(
|
|
210
315
|
logger.warning(
|
211
316
|
f"no html report found; to attach one, create an .html export for your {filepath.suffix} file and then run: lamin save {filepath}"
|
212
317
|
)
|
213
|
-
if report_path is not None and not from_cli:
|
214
|
-
|
215
|
-
|
216
|
-
|
318
|
+
if report_path is not None and is_r_notebook and not from_cli: # R notebooks
|
319
|
+
recently_saved_time = 3 if not is_retry else 20
|
320
|
+
if get_seconds_since_modified(report_path) > recently_saved_time:
|
321
|
+
# the automated retry solution of Jupyter notebooks does not work in RStudio because the execution of the notebook cell
|
322
|
+
# seems to block the event loop of the frontend
|
323
|
+
if not is_retry:
|
324
|
+
logger.warning(get_save_notebook_message_retry())
|
325
|
+
return "retry"
|
326
|
+
else:
|
327
|
+
logger.warning(
|
328
|
+
"the notebook on disk hasn't been saved within the last 20 sec"
|
329
|
+
)
|
330
|
+
save_source_code_and_report = False
|
217
331
|
ln.settings.creation.artifact_silence_missing_run_warning = True
|
218
|
-
#
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
332
|
+
# save source code
|
333
|
+
if save_source_code_and_report:
|
334
|
+
hash, _ = hash_file(source_code_path) # ignore hash_type for now
|
335
|
+
if transform.hash is not None:
|
336
|
+
# check if the hash of the transform source code matches
|
337
|
+
# (for scripts, we already run the same logic in track() - we can deduplicate the call at some point)
|
338
|
+
if hash != transform.hash:
|
339
|
+
response = input(
|
340
|
+
f"You are about to overwrite existing source code (hash '{transform.hash}') for Transform('{transform.uid}')."
|
341
|
+
f" Proceed? (y/n)"
|
342
|
+
)
|
343
|
+
if response == "y":
|
344
|
+
transform.source_code = source_code_path.read_text()
|
345
|
+
transform.hash = hash
|
346
|
+
else:
|
347
|
+
logger.warning("Please re-run `ln.track()` to make a new version")
|
348
|
+
return "rerun-the-notebook"
|
231
349
|
else:
|
232
|
-
logger.
|
233
|
-
return "rerun-the-notebook"
|
350
|
+
logger.debug("source code is already saved")
|
234
351
|
else:
|
235
|
-
|
236
|
-
|
237
|
-
transform.source_code = source_code_path.read_text()
|
238
|
-
transform.hash = hash
|
352
|
+
transform.source_code = source_code_path.read_text()
|
353
|
+
transform.hash = hash
|
239
354
|
|
240
|
-
# track environment
|
355
|
+
# track run environment
|
241
356
|
if run is not None:
|
242
357
|
env_path = ln_setup.settings.cache_dir / f"run_env_pip_{run.uid}.txt"
|
243
358
|
if env_path.exists():
|
@@ -263,50 +378,57 @@ def save_context_core(
|
|
263
378
|
|
264
379
|
# set finished_at
|
265
380
|
if finished_at and run is not None:
|
266
|
-
|
381
|
+
if not from_cli:
|
382
|
+
update_finished_at = True
|
383
|
+
else:
|
384
|
+
update_finished_at = run.finished_at is None
|
385
|
+
if update_finished_at:
|
386
|
+
run.finished_at = datetime.now(timezone.utc)
|
267
387
|
|
268
388
|
# track logs
|
269
389
|
if run is not None and not from_cli and not is_ipynb and not is_r_notebook:
|
270
390
|
save_run_logs(run)
|
271
391
|
|
272
392
|
# track report and set is_consecutive
|
273
|
-
if
|
274
|
-
if
|
275
|
-
if
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
393
|
+
if save_source_code_and_report:
|
394
|
+
if run is not None:
|
395
|
+
if report_path is not None:
|
396
|
+
if is_r_notebook:
|
397
|
+
title_text, report_path = clean_r_notebook_html(report_path)
|
398
|
+
if title_text is not None:
|
399
|
+
transform.description = title_text
|
400
|
+
if run.report_id is not None:
|
401
|
+
hash, _ = hash_file(report_path) # ignore hash_type for now
|
402
|
+
if hash != run.report.hash:
|
403
|
+
response = input(
|
404
|
+
f"You are about to overwrite an existing report (hash '{run.report.hash}') for Run('{run.uid}'). Proceed? (y/n)"
|
405
|
+
)
|
406
|
+
if response == "y":
|
407
|
+
run.report.replace(report_path)
|
408
|
+
run.report.save(upload=True, print_progress=False)
|
409
|
+
else:
|
410
|
+
logger.important("keeping old report")
|
288
411
|
else:
|
289
|
-
logger.important("
|
412
|
+
logger.important("report is already saved")
|
290
413
|
else:
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
414
|
+
report_file = ln.Artifact(
|
415
|
+
report_path,
|
416
|
+
description=f"Report of run {run.uid}",
|
417
|
+
_branch_code=0, # hidden file
|
418
|
+
run=False,
|
419
|
+
)
|
420
|
+
report_file.save(upload=True, print_progress=False)
|
421
|
+
run.report = report_file
|
422
|
+
if is_r_notebook:
|
423
|
+
# this is the "cleaned" report
|
424
|
+
report_path.unlink()
|
425
|
+
logger.debug(
|
426
|
+
f"saved transform.latest_run.report: {transform.latest_run.report}"
|
298
427
|
)
|
299
|
-
|
300
|
-
run.report = report_file
|
301
|
-
if is_r_notebook:
|
302
|
-
# this is the "cleaned" report
|
303
|
-
report_path.unlink()
|
304
|
-
logger.debug(
|
305
|
-
f"saved transform.latest_run.report: {transform.latest_run.report}"
|
306
|
-
)
|
307
|
-
run._is_consecutive = is_consecutive
|
428
|
+
run._is_consecutive = is_consecutive
|
308
429
|
|
309
|
-
|
430
|
+
# save both run & transform records if we arrive here
|
431
|
+
if run is not None:
|
310
432
|
run.save()
|
311
433
|
transform.save()
|
312
434
|
|
@@ -318,19 +440,32 @@ def save_context_core(
|
|
318
440
|
hours = seconds // 3600
|
319
441
|
minutes = (seconds % 3600) // 60
|
320
442
|
secs = seconds % 60
|
321
|
-
formatted_run_time =
|
443
|
+
formatted_run_time = (
|
444
|
+
f"{days}d"
|
445
|
+
if days != 0
|
446
|
+
else "" + f"{hours}h"
|
447
|
+
if hours != 0
|
448
|
+
else "" + f"{minutes}m"
|
449
|
+
if minutes != 0
|
450
|
+
else "" + f"{secs}s"
|
451
|
+
)
|
322
452
|
|
323
453
|
logger.important(
|
324
454
|
f"finished Run('{run.uid[:8]}') after {formatted_run_time} at {format_field_value(run.finished_at)}"
|
325
455
|
)
|
326
456
|
if ln_setup.settings.instance.is_on_hub:
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
457
|
+
instance_slug = ln_setup.settings.instance.slug
|
458
|
+
if save_source_code_and_report:
|
459
|
+
logger.important(
|
460
|
+
f"go to: https://lamin.ai/{instance_slug}/transform/{transform.uid}"
|
461
|
+
)
|
462
|
+
if not from_cli and save_source_code_and_report:
|
332
463
|
thing = "notebook" if (is_ipynb or is_r_notebook) else "script"
|
333
464
|
logger.important(
|
334
|
-
f"
|
465
|
+
f"to update your {thing} from the CLI, run: lamin save {filepath}"
|
335
466
|
)
|
467
|
+
if not save_source_code_and_report:
|
468
|
+
logger.warning(
|
469
|
+
f"did *not* save source code and report -- to do so, run: lamin save {filepath}"
|
470
|
+
)
|
336
471
|
return None
|
lamindb/core/_context.py
CHANGED
@@ -24,7 +24,6 @@ from ._sync_git import get_transform_reference_from_git_repo
|
|
24
24
|
from ._track_environment import track_environment
|
25
25
|
from .exceptions import (
|
26
26
|
InconsistentKey,
|
27
|
-
NotebookNotSaved,
|
28
27
|
TrackNotCalled,
|
29
28
|
UpdateContext,
|
30
29
|
)
|
@@ -201,6 +200,7 @@ class Context:
|
|
201
200
|
self._logging_message_track: str = ""
|
202
201
|
self._logging_message_imports: str = ""
|
203
202
|
self._stream_tracker: LogStreamTracker = LogStreamTracker()
|
203
|
+
self._is_finish_retry: bool = False
|
204
204
|
|
205
205
|
@property
|
206
206
|
def transform(self) -> Transform | None:
|
@@ -307,6 +307,10 @@ class Context:
|
|
307
307
|
) = self._track_source_code(path=path)
|
308
308
|
if description is None:
|
309
309
|
description = self._description
|
310
|
+
# temporarily until the hub displays the key by default
|
311
|
+
# populate the description with the filename again
|
312
|
+
if description is None:
|
313
|
+
description = self._path.name
|
310
314
|
self._create_or_load_transform(
|
311
315
|
description=description,
|
312
316
|
transform_ref=transform_ref,
|
@@ -494,15 +498,19 @@ class Context:
|
|
494
498
|
if aux_transform.key in self._path.as_posix():
|
495
499
|
key = aux_transform.key
|
496
500
|
if (
|
497
|
-
#
|
498
|
-
aux_transform.
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
501
|
+
# has to be the same user
|
502
|
+
aux_transform.created_by_id == ln_setup.settings.user.id
|
503
|
+
and (
|
504
|
+
# if the transform source code wasn't yet saved
|
505
|
+
aux_transform.source_code is None
|
506
|
+
# if the transform source code is unchanged
|
507
|
+
# if aux_transform.type == "notebook", we anticipate the user makes changes to the notebook source code
|
508
|
+
# in an interactive session, hence we *pro-actively bump* the version number by setting `revises`
|
509
|
+
# in the second part of the if condition even though the source code is unchanged at point of running track()
|
510
|
+
or (
|
511
|
+
aux_transform.hash == hash
|
512
|
+
and aux_transform.type != "notebook"
|
513
|
+
)
|
506
514
|
)
|
507
515
|
):
|
508
516
|
uid = aux_transform.uid
|
@@ -514,9 +522,13 @@ class Context:
|
|
514
522
|
aux_transform.hash == hash
|
515
523
|
and aux_transform.type == "notebook"
|
516
524
|
):
|
517
|
-
message += " --
|
525
|
+
message += " -- anticipating changes"
|
518
526
|
elif aux_transform.hash != hash:
|
519
|
-
message += "
|
527
|
+
message += "" # could log "source code changed", but this seems too much
|
528
|
+
elif (
|
529
|
+
aux_transform.created_by_id != ln_setup.settings.user.id
|
530
|
+
):
|
531
|
+
message += f" -- {aux_transform.created_by.handle} already works on this draft"
|
520
532
|
message += f", creating new version '{uid}'"
|
521
533
|
revises = aux_transform
|
522
534
|
found_key = True
|
@@ -613,7 +625,7 @@ class Context:
|
|
613
625
|
and not transform_was_saved
|
614
626
|
):
|
615
627
|
raise UpdateContext(
|
616
|
-
f'{transform.created_by.
|
628
|
+
f'{transform.created_by.name} ({transform.created_by.handle}) already works on this draft {transform.type}.\n\nPlease create a revision via `ln.track("{uid[:-4]}{increment_base62(uid[-4:])}")` or a new transform with a *different* filedescription and `ln.track("{ids.base62_12()}0000")`.'
|
617
629
|
)
|
618
630
|
# check whether transform source code was already saved
|
619
631
|
if transform_was_saved:
|
@@ -648,12 +660,12 @@ class Context:
|
|
648
660
|
|
649
661
|
- writes a timestamp: `run.finished_at`
|
650
662
|
- saves the source code: `transform.source_code`
|
663
|
+
- saves a run report: `run.report`
|
651
664
|
|
652
665
|
When called in the last cell of a notebook:
|
653
666
|
|
667
|
+
- prompts to save the notebook in your editor right before
|
654
668
|
- prompts for user input if not consecutively executed
|
655
|
-
- requires to save the notebook in your editor right before
|
656
|
-
- saves a run report: `run.report`
|
657
669
|
|
658
670
|
Args:
|
659
671
|
ignore_non_consecutive: Whether to ignore if a notebook was non-consecutively executed.
|
@@ -670,8 +682,6 @@ class Context:
|
|
670
682
|
|
671
683
|
"""
|
672
684
|
from lamindb._finish import (
|
673
|
-
get_save_notebook_message,
|
674
|
-
get_seconds_since_modified,
|
675
685
|
save_context_core,
|
676
686
|
)
|
677
687
|
|
@@ -686,24 +696,17 @@ class Context:
|
|
686
696
|
self.run.save()
|
687
697
|
# nothing else to do
|
688
698
|
return None
|
689
|
-
|
690
|
-
import nbproject
|
691
|
-
|
692
|
-
# it might be that the user modifies the title just before ln.finish()
|
693
|
-
if (
|
694
|
-
nbproject_title := nbproject.meta.live.title
|
695
|
-
) != self.transform.description:
|
696
|
-
self.transform.description = nbproject_title
|
697
|
-
self.transform.save()
|
698
|
-
if get_seconds_since_modified(self._path) > 2 and not ln_setup._TESTING:
|
699
|
-
raise NotebookNotSaved(get_save_notebook_message())
|
700
|
-
save_context_core(
|
699
|
+
return_code = save_context_core(
|
701
700
|
run=self.run,
|
702
701
|
transform=self.run.transform,
|
703
702
|
filepath=self._path,
|
704
703
|
finished_at=True,
|
705
704
|
ignore_non_consecutive=ignore_non_consecutive,
|
705
|
+
is_retry=self._is_finish_retry,
|
706
706
|
)
|
707
|
+
if return_code == "retry":
|
708
|
+
self._is_finish_retry = True
|
709
|
+
return None
|
707
710
|
if self.transform.type != "notebook":
|
708
711
|
self._stream_tracker.finish()
|
709
712
|
# reset the context attributes so that somebody who runs `track()` after finish
|
lamindb/core/_feature_manager.py
CHANGED
lamindb/core/loaders.py
CHANGED
@@ -40,7 +40,7 @@ try:
|
|
40
40
|
except ImportError:
|
41
41
|
|
42
42
|
def load_anndata_zarr(storepath): # type: ignore
|
43
|
-
raise ImportError("Please install zarr: pip install zarr")
|
43
|
+
raise ImportError("Please install zarr: pip install zarr<=2.18.4")
|
44
44
|
|
45
45
|
|
46
46
|
is_run_from_ipython = getattr(builtins, "__IPYTHON__", False)
|
lamindb/core/storage/paths.py
CHANGED
@@ -139,7 +139,20 @@ def store_file_or_folder(
|
|
139
139
|
local_path = UPath(local_path)
|
140
140
|
if not isinstance(storage_path, LocalPathClasses):
|
141
141
|
# this uploads files and directories
|
142
|
-
|
142
|
+
if local_path.is_dir():
|
143
|
+
create_folder = False
|
144
|
+
try:
|
145
|
+
# if storage_path already exists we need to delete it
|
146
|
+
# if local_path is a directory
|
147
|
+
# to replace storage_path correctly
|
148
|
+
if storage_path.stat().as_info()["type"] == "directory":
|
149
|
+
storage_path.rmdir()
|
150
|
+
else:
|
151
|
+
storage_path.unlink()
|
152
|
+
except (FileNotFoundError, PermissionError):
|
153
|
+
pass
|
154
|
+
else:
|
155
|
+
create_folder = None
|
143
156
|
storage_path.upload_from(
|
144
157
|
local_path, create_folder=create_folder, print_progress=print_progress
|
145
158
|
)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Generated by Django 5.2 on 2025-01-21 17:03
|
2
|
+
|
3
|
+
from django.db import migrations
|
4
|
+
|
5
|
+
import lamindb.base.fields
|
6
|
+
|
7
|
+
|
8
|
+
class Migration(migrations.Migration):
|
9
|
+
dependencies = [
|
10
|
+
("lamindb", "0080_polish_lamindbv1"),
|
11
|
+
]
|
12
|
+
|
13
|
+
operations = [
|
14
|
+
migrations.AlterField(
|
15
|
+
model_name="collection",
|
16
|
+
name="description",
|
17
|
+
field=lamindb.base.fields.TextField(
|
18
|
+
blank=True, db_index=True, default=None, null=True
|
19
|
+
),
|
20
|
+
),
|
21
|
+
]
|
lamindb/models.py
CHANGED
@@ -1679,7 +1679,7 @@ class ULabel(Record, HasParents, CanCurate, TracksRun, TracksUpdates):
|
|
1679
1679
|
)
|
1680
1680
|
"""A universal random id, valid across DB instances."""
|
1681
1681
|
name: str = CharField(max_length=150, db_index=True)
|
1682
|
-
"""Name or title of ulabel
|
1682
|
+
"""Name or title of ulabel."""
|
1683
1683
|
type: ULabel | None = ForeignKey("self", PROTECT, null=True, related_name="records")
|
1684
1684
|
"""Type of ulabel, e.g., `"donor"`, `"split"`, etc.
|
1685
1685
|
|
@@ -1843,7 +1843,7 @@ class Feature(Record, CanCurate, TracksRun, TracksUpdates):
|
|
1843
1843
|
)
|
1844
1844
|
"""Universal id, valid across DB instances."""
|
1845
1845
|
name: str = CharField(max_length=150, db_index=True, unique=True)
|
1846
|
-
"""Name of feature (`unique=True`)."""
|
1846
|
+
"""Name of feature (hard unique constraint `unique=True`)."""
|
1847
1847
|
dtype: FeatureDtype = CharField(db_index=True)
|
1848
1848
|
"""Data type (:class:`~lamindb.base.types.FeatureDtype`).
|
1849
1849
|
|
@@ -2791,7 +2791,7 @@ class Artifact(Record, IsVersioned, TracksRun, TracksUpdates):
|
|
2791
2791
|
|
2792
2792
|
def replace(
|
2793
2793
|
self,
|
2794
|
-
data: UPathStr,
|
2794
|
+
data: UPathStr | pd.DataFrame | AnnData | MuData,
|
2795
2795
|
run: Run | None = None,
|
2796
2796
|
format: str | None = None,
|
2797
2797
|
) -> None:
|
@@ -3008,7 +3008,10 @@ class Collection(Record, IsVersioned, TracksRun, TracksUpdates):
|
|
3008
3008
|
"""Universal id, valid across DB instances."""
|
3009
3009
|
key: str = CharField(db_index=True)
|
3010
3010
|
"""Name or path-like key."""
|
3011
|
-
|
3011
|
+
# these here is the only case in which we use a TextField
|
3012
|
+
# for description; we do so because users had descriptions exceeding 255 chars
|
3013
|
+
# in their instances
|
3014
|
+
description: str | None = TextField(null=True, db_index=True)
|
3012
3015
|
"""A description or title."""
|
3013
3016
|
hash: str | None = CharField(max_length=HASH_LENGTH, db_index=True, null=True)
|
3014
3017
|
"""Hash of collection content. 86 base64 chars allow to store 64 bytes, 512 bits."""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: lamindb
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.5
|
4
4
|
Summary: A data framework for biology.
|
5
5
|
Author-email: Lamin Labs <open-source@lamin.ai>
|
6
6
|
Requires-Python: >=3.10,<3.13
|
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
11
11
|
Requires-Dist: lamin_utils==0.13.10
|
12
|
-
Requires-Dist: lamin_cli==1.0.
|
12
|
+
Requires-Dist: lamin_cli==1.0.7
|
13
13
|
Requires-Dist: lamindb_setup[aws]==1.0.3
|
14
14
|
Requires-Dist: pyarrow
|
15
15
|
Requires-Dist: typing_extensions!=4.6.0
|
@@ -23,6 +23,7 @@ Requires-Dist: psycopg2-binary
|
|
23
23
|
Requires-Dist: bionty==1.0.0 ; extra == "bionty"
|
24
24
|
Requires-Dist: cellregistry ; extra == "cellregistry"
|
25
25
|
Requires-Dist: clinicore==1.0.0 ; extra == "clinicore"
|
26
|
+
Requires-Dist: tomlkit ; extra == "dev"
|
26
27
|
Requires-Dist: line_profiler ; extra == "dev"
|
27
28
|
Requires-Dist: pre-commit ; extra == "dev"
|
28
29
|
Requires-Dist: nox ; extra == "dev"
|
@@ -1,9 +1,9 @@
|
|
1
|
-
lamindb/__init__.py,sha256=
|
2
|
-
lamindb/_artifact.py,sha256=
|
1
|
+
lamindb/__init__.py,sha256=G-HK8IuAG4oCMtmrpeHdnj3HBNt-6w8m6QiPLbexTK8,2255
|
2
|
+
lamindb/_artifact.py,sha256=IEbB4rr3BUoadNRGIgVL93w_uMVUblnvPWZ5wqpWocY,47220
|
3
3
|
lamindb/_can_curate.py,sha256=pIu9Ylgq5biUd_67rRbAHg9tkXSQrxMRM8TnVboL9YA,20341
|
4
4
|
lamindb/_collection.py,sha256=j2yTfR9v-NGUI85JfQk7vOQNExkWE5H_ulsSlBh1AOI,14456
|
5
5
|
lamindb/_feature.py,sha256=qpUZfmdxUpQFLq5GciTn6KgALL19tCs0raHhzJgm7Lo,6311
|
6
|
-
lamindb/_finish.py,sha256=
|
6
|
+
lamindb/_finish.py,sha256=R-wRJi881WJhrC6uu4t7sls9oFvKJSTXtwVEM38LSnQ,18671
|
7
7
|
lamindb/_from_values.py,sha256=uO3IfYzAI8VDDTqlbLzsZtawSYFS-Qzd_ZWwKGhH90o,14231
|
8
8
|
lamindb/_is_versioned.py,sha256=6_LBAKD_fng6BReqitJUIxTUaQok3AeIpNnE_D8kHnQ,1293
|
9
9
|
lamindb/_parents.py,sha256=PA--_ZH3PNqIVW0PpuYk9d4DAVlHUBPN-dN0rFUKUN0,17238
|
@@ -18,7 +18,7 @@ lamindb/_transform.py,sha256=LYFf8gScJrYLMZJECLYZ5nrW2vLPObdzRP47md-Tq-s,5731
|
|
18
18
|
lamindb/_ulabel.py,sha256=YTiUCYrcEqyUKD8nZO4iOqiyYnUP5bW_r7yry4KSeWA,2068
|
19
19
|
lamindb/_utils.py,sha256=LGdiW4k3GClLz65vKAVRkL6Tw-Gkx9DWAdez1jyA5bE,428
|
20
20
|
lamindb/_view.py,sha256=c4eN5hcBlg3TVnljKefbyWAq0eBncjMp2xQcb5OaGWg,4982
|
21
|
-
lamindb/models.py,sha256=
|
21
|
+
lamindb/models.py,sha256=6WF2uFkqrcHiCMVNWlgcDGDNlRQ-WjXs2W-1Ryty3k4,147747
|
22
22
|
lamindb/base/__init__.py,sha256=J0UpYObi9hJBFyBpAXp4wB3DaJx48R2SaUeB4wjiFvc,267
|
23
23
|
lamindb/base/fields.py,sha256=RdwYHQmB7B-jopD_K2QNL5vjhOelu7DWGgqQItXr3pg,8024
|
24
24
|
lamindb/base/ids.py,sha256=WzHWiHZtlRUKqxz_p-76ks_JSW669ztvriE7Z3A0yHg,1736
|
@@ -26,11 +26,11 @@ lamindb/base/types.py,sha256=JfZk0xmhLsWusU0s4SNjhRnQ52mn-cSiG5Gf4SsACBs,1227
|
|
26
26
|
lamindb/base/users.py,sha256=g4ZLQb6eey66FO9eEumbfDpJi_FZZsiLVe2Frz9JuLI,978
|
27
27
|
lamindb/base/validation.py,sha256=Azz9y2-x0cPum4yULXMG3Yzra03mcVYzcKTiI23HgxE,2287
|
28
28
|
lamindb/core/__init__.py,sha256=4AGZqt5g8k3jFX53IXdezQR4Gf7JmMBLZRyTJJzS4sI,1628
|
29
|
-
lamindb/core/_context.py,sha256=
|
29
|
+
lamindb/core/_context.py,sha256=gacr4xG1J14kAVkPmbDIXIVq0qpCPssatB2sXIZPuck,28856
|
30
30
|
lamindb/core/_data.py,sha256=xg_St591OPCzLhaZAlGxVd8QkDxZsxDc_yEwY8Kop8w,19017
|
31
31
|
lamindb/core/_describe.py,sha256=3Z1xi9eKIBkYuPW9ctdWvFaGZym8mI9FcwyZF3a6YVo,4885
|
32
32
|
lamindb/core/_django.py,sha256=vPY4wJ4bf3a1uz5bhukCCF_mngR_9w2Ot3nvWZpa204,7629
|
33
|
-
lamindb/core/_feature_manager.py,sha256=
|
33
|
+
lamindb/core/_feature_manager.py,sha256=YU4gqKvgJEMh0NOvGqZrrB8nSgdjBGnw1bhSVb1uVGE,47996
|
34
34
|
lamindb/core/_label_manager.py,sha256=OKMXpEM5FVhzGm-PU3-jQ1ZcQI_gCTgP5X1Vvhs9XF4,11876
|
35
35
|
lamindb/core/_mapped_collection.py,sha256=btDGl8V3Vi_MDy-9vIi40p8ejL2Kb0dO4rhdCiNWpxc,24870
|
36
36
|
lamindb/core/_settings.py,sha256=haLHE1dhog_Sz6gnecTW8E548njWH5nVt8mTFPEs6ZM,5733
|
@@ -38,7 +38,7 @@ lamindb/core/_sync_git.py,sha256=xu1o6zlBD_pTRpBPVXiHKOI2tmtGaUfXvuuII2AAfM4,587
|
|
38
38
|
lamindb/core/_track_environment.py,sha256=nVEO98P7ZrUWyixMI3AdoD7vcUszIVciZwgPOsQUsNU,814
|
39
39
|
lamindb/core/exceptions.py,sha256=QnvqzMPIHrjaVnyqCBXtut3Jy5LYtV1GBNjGO7MISHY,1691
|
40
40
|
lamindb/core/fields.py,sha256=zM6G7CiE6mU_5heLWwIhE3d5LqAjPCEwgs0eechFm0c,175
|
41
|
-
lamindb/core/loaders.py,sha256=
|
41
|
+
lamindb/core/loaders.py,sha256=d3s2eF2rf66wW350HZQYNZW4S96k3RaQYgvks01Kt0Q,4767
|
42
42
|
lamindb/core/relations.py,sha256=NLr2s4xreh4_93UyoV57QNyTBi-swokQyRcotZzTqAI,3428
|
43
43
|
lamindb/core/types.py,sha256=DS4uXaCswAW9tqz4MZ_sYc1_QZ6il--Z4x8HeHNQgRw,162
|
44
44
|
lamindb/core/versioning.py,sha256=tUZthgDwbz7UBx1wx_MsdsXXsltE1E9clapmRkmvKlU,4953
|
@@ -55,7 +55,7 @@ lamindb/core/storage/_tiledbsoma.py,sha256=g3qNISsNfrxypU36vqFB-QBYuCpsmBLMKWYVl
|
|
55
55
|
lamindb/core/storage/_valid_suffixes.py,sha256=vUSeQ4s01rdhD_vSd6wKmFBsgMJAKkBMnL_T9Y1znMg,501
|
56
56
|
lamindb/core/storage/_zarr.py,sha256=sVd9jVt2q91maeL6GAqMhT6sqtD04vQRfxY3mvIUQlc,3854
|
57
57
|
lamindb/core/storage/objects.py,sha256=5vM2T_upuzrXt2b7fQeQ2FUO710-FRbubxTzKzV2ECU,1812
|
58
|
-
lamindb/core/storage/paths.py,sha256=
|
58
|
+
lamindb/core/storage/paths.py,sha256=ahYJz6PmySEw544soQS-6ug8npDnwDm18lgvCFn7Q3w,6794
|
59
59
|
lamindb/core/subsettings/__init__.py,sha256=j6G9WAJLK-x9FzPSFw-HJUmOseZKGTbK-oLTKI_X_zs,126
|
60
60
|
lamindb/core/subsettings/_creation_settings.py,sha256=54mfMH_osC753hpxcl7Dq1rwBD2LHnWveXtQpkLBITE,1194
|
61
61
|
lamindb/curators/__init__.py,sha256=XGHnTEFB-Q4XphR0AWLXhITqhAZqkqEzuJJq1Ypb-zU,92357
|
@@ -92,10 +92,11 @@ lamindb/migrations/0077_lamindbv1_part6b.py,sha256=v7k8OZX9o5ppSJU_yhHlIXGTobTm3
|
|
92
92
|
lamindb/migrations/0078_lamindbv1_part6c.py,sha256=RWRXBwyyQ_rFTN5kwstBziV6tqHJcGYI2vsFmuYCCz0,17084
|
93
93
|
lamindb/migrations/0079_alter_rundata_value_json_and_more.py,sha256=yQmbs8yWrFLOVQJqAfzLNMZOqTSnXyG-mQgpO7ls1u8,995
|
94
94
|
lamindb/migrations/0080_polish_lamindbv1.py,sha256=VfCwJtHlBsMPIyFQ2oh24oWkiRXjDvXRpKe5fBZ63aM,17660
|
95
|
+
lamindb/migrations/0081_revert_textfield_collection.py,sha256=uHuJ0W4Ips7BrnQnQBGPMn2eFQz29a1QAdHzN7XlDxo,490
|
95
96
|
lamindb/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
96
97
|
lamindb/setup/__init__.py,sha256=OwZpZzPDv5lPPGXZP7-zK6UdO4FHvvuBh439yZvIp3A,410
|
97
98
|
lamindb/setup/core/__init__.py,sha256=SevlVrc2AZWL3uALbE5sopxBnIZPWZ1IB0NBDudiAL8,167
|
98
|
-
lamindb-1.0.
|
99
|
-
lamindb-1.0.
|
100
|
-
lamindb-1.0.
|
101
|
-
lamindb-1.0.
|
99
|
+
lamindb-1.0.5.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
100
|
+
lamindb-1.0.5.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
|
101
|
+
lamindb-1.0.5.dist-info/METADATA,sha256=keGjzSSAZGJK8iXJ778p0xgzolFAorxvMiaJt06a0Gs,2651
|
102
|
+
lamindb-1.0.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|