lamindb 1.0.4__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 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.4"
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
- raise ValueError("Can only replace with a local file not in any Storage.")
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 = ("", ".h5", ".hdf5", ".h5ad", ".zarr", ".tiledbsoma") + PYARROW_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
- return f"Please save the notebook in your editor (shortcut `{get_shortcut()}`) within 2 sec before calling `finish()`"
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 resaved finish error if present
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("ename", None) == "NotebookNotSaved":
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 "NotebookNotSaved" in cleaned_content:
133
- orig_error_message = f"NotebookNotSaved: {get_save_notebook_message()}"
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 "NotebookNotSaved" in cleaned_content:
199
+ if "to save the notebook in your editor" in cleaned_content:
139
200
  orig_error_message = orig_error_message.replace(
140
- " `finish()`", "\n`finish()`"
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
- # for notebooks, we need more work
171
- if is_ipynb:
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
- if get_seconds_since_modified(report_path) > 2 and not ln_setup._TESTING:
215
- # this can happen when auto-knitting an html with RStudio
216
- raise NotebookNotSaved(get_save_notebook_message())
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
- # track source code
219
- hash, _ = hash_file(source_code_path) # ignore hash_type for now
220
- if transform.hash is not None:
221
- # check if the hash of the transform source code matches
222
- # (for scripts, we already run the same logic in track() - we can deduplicate the call at some point)
223
- if hash != transform.hash:
224
- response = input(
225
- f"You are about to overwrite existing source code (hash '{transform.hash}') for Transform('{transform.uid}')."
226
- f" Proceed? (y/n)"
227
- )
228
- if response == "y":
229
- transform.source_code = source_code_path.read_text()
230
- transform.hash = hash
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.warning("Please re-run `ln.track()` to make a new version")
233
- return "rerun-the-notebook"
350
+ logger.debug("source code is already saved")
234
351
  else:
235
- logger.debug("source code is already saved")
236
- else:
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
- run.finished_at = datetime.now(timezone.utc)
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 run is not None:
274
- if report_path is not None:
275
- if is_r_notebook:
276
- title_text, report_path = clean_r_notebook_html(report_path)
277
- if title_text is not None:
278
- transform.description = title_text
279
- if run.report_id is not None:
280
- hash, _ = hash_file(report_path) # ignore hash_type for now
281
- if hash != run.report.hash:
282
- response = input(
283
- f"You are about to overwrite an existing report (hash '{run.report.hash}') for Run('{run.uid}'). Proceed? (y/n)"
284
- )
285
- if response == "y":
286
- run.report.replace(report_path)
287
- run.report.save(upload=True, print_progress=False)
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("keeping old report")
412
+ logger.important("report is already saved")
290
413
  else:
291
- logger.important("report is already saved")
292
- else:
293
- report_file = ln.Artifact(
294
- report_path,
295
- description=f"Report of run {run.uid}",
296
- _branch_code=0, # hidden file
297
- run=False,
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
- report_file.save(upload=True, print_progress=False)
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
- # save both run & transform records if we arrive here
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 = f"{days}d {hours}h {minutes}m {secs}s"
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
- identifier = ln_setup.settings.instance.slug
328
- logger.important(
329
- f"go to: https://lamin.ai/{identifier}/transform/{transform.uid}"
330
- )
331
- if not from_cli:
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"if you want to update your {thing} without re-running it, use `lamin save {filepath}`"
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
- # if the transform source code wasn't yet saved
498
- aux_transform.source_code is None
499
- # if the transform source code is unchanged
500
- # if aux_transform.type == "notebook", we anticipate the user makes changes to the notebook source code
501
- # in an interactive session, hence we *pro-actively bump* the version number by setting `revises`
502
- # in the second part of the if condition even though the source code is unchanged at point of running track()
503
- or (
504
- aux_transform.hash == hash
505
- and aux_transform.type != "notebook"
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 += " -- notebook source code is unchanged, but anticipating changes during this run"
525
+ message += " -- anticipating changes"
518
526
  elif aux_transform.hash != hash:
519
- message += " -- source code changed"
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.description} ({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")`.'
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
- if is_run_from_ipython: # notebooks
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
@@ -466,7 +466,7 @@ def describe_features(
466
466
  Text.assemble(
467
467
  ("Dataset features", "bold bright_magenta"),
468
468
  ("/", "dim"),
469
- ("._schemas_m2m", "dim bold"),
469
+ ("schema", "dim bold"),
470
470
  )
471
471
  )
472
472
  for child in int_features_tree_children:
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)
@@ -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
- create_folder = False if local_path.is_dir() else None
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
  )
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 (`unique=True`)."""
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lamindb
3
- Version: 1.0.4
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.4
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=bKJLQZmbvlVCRAU9HxyoOpMUjG7p-gqbGRxCyCFXy_8,2255
2
- lamindb/_artifact.py,sha256=22pKCA05PoIAgP2xszCHUovZ1VbMGIyrQqNLs5xDG_s,46580
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=aMN9CwTGAXeqWSDvCumVLztHtKttwI9z52mdz4ue5Uw,13362
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=9zjdIPuuBdjKi3V-LqdPK0uMrVocGhosOi29YViuwgo,147706
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=na1n_Lpj7Dvxjk1KB5Di4jEdhH-d6Jf757fvY1awEGM,28591
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=Omx9t2LYfiTSBf1wOAd9PP-vkWFTyRlKETXqRc3Cdqc,48003
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=7WnEgoklcMtU87SLDaxpLaFQ2U5jz20DAh9aJnpLhD0,4759
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=XXEy51qCw0z497y6ZEN_SULY3oXtA4XlanHo-TGK7jY,6302
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
@@ -96,7 +96,7 @@ lamindb/migrations/0081_revert_textfield_collection.py,sha256=uHuJ0W4Ips7BrnQnQB
96
96
  lamindb/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
97
  lamindb/setup/__init__.py,sha256=OwZpZzPDv5lPPGXZP7-zK6UdO4FHvvuBh439yZvIp3A,410
98
98
  lamindb/setup/core/__init__.py,sha256=SevlVrc2AZWL3uALbE5sopxBnIZPWZ1IB0NBDudiAL8,167
99
- lamindb-1.0.4.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
100
- lamindb-1.0.4.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
101
- lamindb-1.0.4.dist-info/METADATA,sha256=9Y2mkw39jPE4YdbNPQoGwliwHBVWwN1o15jBG5OwtCU,2611
102
- lamindb-1.0.4.dist-info/RECORD,,
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,,