lamindb 1.0.4__py3-none-any.whl → 1.1.0__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 +14 -5
- lamindb/_artifact.py +174 -57
- lamindb/_can_curate.py +27 -8
- lamindb/_collection.py +85 -51
- lamindb/_feature.py +177 -41
- lamindb/_finish.py +222 -81
- lamindb/_from_values.py +83 -98
- lamindb/_parents.py +4 -4
- lamindb/_query_set.py +59 -17
- lamindb/_record.py +171 -53
- lamindb/_run.py +4 -4
- lamindb/_save.py +33 -10
- lamindb/_schema.py +135 -38
- lamindb/_storage.py +1 -1
- lamindb/_tracked.py +106 -0
- lamindb/_transform.py +21 -8
- lamindb/_ulabel.py +5 -14
- lamindb/base/validation.py +2 -6
- lamindb/core/__init__.py +13 -14
- lamindb/core/_context.py +39 -36
- lamindb/core/_data.py +29 -25
- lamindb/core/_describe.py +1 -1
- lamindb/core/_django.py +1 -1
- lamindb/core/_feature_manager.py +54 -44
- lamindb/core/_label_manager.py +4 -4
- lamindb/core/_mapped_collection.py +20 -7
- lamindb/core/datasets/__init__.py +6 -1
- lamindb/core/datasets/_core.py +12 -11
- lamindb/core/datasets/_small.py +66 -20
- lamindb/core/exceptions.py +1 -90
- lamindb/core/loaders.py +7 -13
- lamindb/core/relations.py +6 -4
- lamindb/core/storage/_anndata_accessor.py +41 -0
- lamindb/core/storage/_backed_access.py +2 -2
- lamindb/core/storage/_pyarrow_dataset.py +25 -15
- lamindb/core/storage/_tiledbsoma.py +56 -12
- lamindb/core/storage/paths.py +41 -22
- lamindb/core/subsettings/_creation_settings.py +4 -16
- lamindb/curators/__init__.py +2168 -833
- lamindb/curators/_cellxgene_schemas/__init__.py +26 -0
- lamindb/curators/_cellxgene_schemas/schema_versions.yml +104 -0
- lamindb/errors.py +96 -0
- lamindb/integrations/_vitessce.py +3 -3
- lamindb/migrations/0069_squashed.py +76 -75
- lamindb/migrations/0075_lamindbv1_part5.py +4 -5
- lamindb/migrations/0082_alter_feature_dtype.py +21 -0
- lamindb/migrations/0083_alter_feature_is_type_alter_flextable_is_type_and_more.py +94 -0
- lamindb/migrations/0084_alter_schemafeature_feature_and_more.py +35 -0
- lamindb/migrations/0085_alter_feature_is_type_alter_flextable_is_type_and_more.py +63 -0
- lamindb/migrations/0086_various.py +95 -0
- lamindb/migrations/0087_rename__schemas_m2m_artifact_feature_sets_and_more.py +41 -0
- lamindb/migrations/0088_schema_components.py +273 -0
- lamindb/migrations/0088_squashed.py +4372 -0
- lamindb/models.py +423 -156
- {lamindb-1.0.4.dist-info → lamindb-1.1.0.dist-info}/METADATA +10 -7
- lamindb-1.1.0.dist-info/RECORD +95 -0
- lamindb/curators/_spatial.py +0 -528
- lamindb/migrations/0052_squashed.py +0 -1261
- lamindb/migrations/0053_alter_featureset_hash_alter_paramvalue_created_by_and_more.py +0 -57
- lamindb/migrations/0054_alter_feature_previous_runs_and_more.py +0 -35
- lamindb/migrations/0055_artifact_type_artifactparamvalue_and_more.py +0 -61
- lamindb/migrations/0056_rename_ulabel_ref_is_name_artifactulabel_label_ref_is_name_and_more.py +0 -22
- lamindb/migrations/0057_link_models_latest_report_and_others.py +0 -356
- lamindb/migrations/0058_artifact__actions_collection__actions.py +0 -22
- lamindb/migrations/0059_alter_artifact__accessor_alter_artifact__hash_type_and_more.py +0 -31
- lamindb/migrations/0060_alter_artifact__actions.py +0 -22
- lamindb/migrations/0061_alter_collection_meta_artifact_alter_run_environment_and_more.py +0 -45
- lamindb/migrations/0062_add_is_latest_field.py +0 -32
- lamindb/migrations/0063_populate_latest_field.py +0 -45
- lamindb/migrations/0064_alter_artifact_version_alter_collection_version_and_more.py +0 -33
- lamindb/migrations/0065_remove_collection_feature_sets_and_more.py +0 -22
- lamindb/migrations/0066_alter_artifact__feature_values_and_more.py +0 -352
- lamindb/migrations/0067_alter_featurevalue_unique_together_and_more.py +0 -20
- lamindb/migrations/0068_alter_artifactulabel_unique_together_and_more.py +0 -20
- lamindb/migrations/0069_alter_artifact__accessor_alter_artifact__hash_type_and_more.py +0 -1294
- lamindb-1.0.4.dist-info/RECORD +0 -102
- {lamindb-1.0.4.dist-info → lamindb-1.1.0.dist-info}/LICENSE +0 -0
- {lamindb-1.0.4.dist-info → lamindb-1.1.0.dist-info}/WHEEL +0 -0
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:
|
@@ -34,7 +96,7 @@ def save_run_logs(run: Run, save_run: bool = False) -> None:
|
|
34
96
|
if logs_path.exists():
|
35
97
|
if run.report is not None:
|
36
98
|
logger.important("overwriting run.report")
|
37
|
-
artifact = Artifact(
|
99
|
+
artifact = Artifact( # type: ignore
|
38
100
|
logs_path,
|
39
101
|
description=f"log streams of run {run.uid}",
|
40
102
|
_branch_code=0,
|
@@ -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
|
@@ -97,7 +159,7 @@ def notebook_to_report(notebook_path: Path, output_path: Path) -> None:
|
|
97
159
|
output_path.write_text(html, encoding="utf-8")
|
98
160
|
|
99
161
|
|
100
|
-
def notebook_to_script(
|
162
|
+
def notebook_to_script( # type: ignore
|
101
163
|
transform: Transform, notebook_path: Path, script_path: Path | None = None
|
102
164
|
) -> None | str:
|
103
165
|
import jupytext
|
@@ -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,35 @@ 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
|
+
# the recently_saved_time needs to be very low for the first check
|
211
|
+
# because an accidental save (e.g. via auto-save) might otherwise lead
|
212
|
+
# to upload of an outdated notebook
|
213
|
+
# also see implementation for R notebooks below
|
214
|
+
offset_saved_time = 0.3 if not is_finish_retry else 20
|
215
|
+
for retry in range(30):
|
216
|
+
recently_saved_time = offset_saved_time + retry # sleep time is 1 sec
|
217
|
+
if get_seconds_since_modified(filepath) > recently_saved_time:
|
218
|
+
if retry == 0:
|
219
|
+
prefix = f"{LEVEL_TO_COLORS[20]}{LEVEL_TO_ICONS[20]}{RESET_COLOR}"
|
220
|
+
print(f"{prefix} {get_save_notebook_message()}", end=" ")
|
221
|
+
elif retry == 9:
|
222
|
+
print(".", end="\n")
|
223
|
+
elif retry == 4:
|
224
|
+
print(". still waiting ", end="")
|
225
|
+
else:
|
226
|
+
print(".", end="")
|
227
|
+
sleep(1)
|
228
|
+
else:
|
229
|
+
if retry > 0:
|
230
|
+
prefix = f"{LEVEL_TO_COLORS[25]}{LEVEL_TO_ICONS[25]}{RESET_COLOR}"
|
231
|
+
print(f" {prefix}")
|
232
|
+
# filepath was recently saved, return True
|
233
|
+
return True
|
234
|
+
# if we arrive here, no save event occured, return False
|
235
|
+
return False
|
236
|
+
|
237
|
+
|
148
238
|
def save_context_core(
|
149
239
|
*,
|
150
240
|
run: Run | None,
|
@@ -153,6 +243,7 @@ def save_context_core(
|
|
153
243
|
finished_at: bool = False,
|
154
244
|
ignore_non_consecutive: bool | None = None,
|
155
245
|
from_cli: bool = False,
|
246
|
+
is_retry: bool = False,
|
156
247
|
) -> str | None:
|
157
248
|
import lamindb as ln
|
158
249
|
from lamindb.models import (
|
@@ -167,12 +258,29 @@ def save_context_core(
|
|
167
258
|
is_r_notebook = filepath.suffix in {".qmd", ".Rmd"}
|
168
259
|
source_code_path = filepath
|
169
260
|
report_path: Path | None = None
|
170
|
-
|
171
|
-
if
|
261
|
+
save_source_code_and_report = True
|
262
|
+
if is_run_from_ipython: # python notebooks in interactive session
|
263
|
+
import nbproject
|
264
|
+
|
265
|
+
# it might be that the user modifies the title just before ln.finish()
|
266
|
+
if (nbproject_title := nbproject.meta.live.title) != transform.description:
|
267
|
+
transform.description = nbproject_title
|
268
|
+
transform.save()
|
269
|
+
if not ln_setup._TESTING:
|
270
|
+
save_source_code_and_report = check_filepath_recently_saved(
|
271
|
+
filepath, is_retry
|
272
|
+
)
|
273
|
+
if not save_source_code_and_report and not is_retry:
|
274
|
+
logger.warning(get_save_notebook_message_retry())
|
275
|
+
return "retry"
|
276
|
+
elif not save_source_code_and_report:
|
277
|
+
logger.warning(
|
278
|
+
"the notebook on disk wasn't saved within the last 10 sec"
|
279
|
+
)
|
280
|
+
if is_ipynb: # could be from CLI outside interactive session
|
172
281
|
try:
|
173
282
|
import jupytext # noqa: F401
|
174
283
|
from nbproject.dev import (
|
175
|
-
check_consecutiveness,
|
176
284
|
read_notebook,
|
177
285
|
)
|
178
286
|
except ImportError:
|
@@ -185,7 +293,9 @@ def save_context_core(
|
|
185
293
|
)
|
186
294
|
if not is_consecutive:
|
187
295
|
response = "n" # ignore_non_consecutive == False
|
188
|
-
if ignore_non_consecutive is None:
|
296
|
+
if ignore_non_consecutive is None: # only print warning
|
297
|
+
response = "y" # we already printed the warning
|
298
|
+
else: # ask user to confirm
|
189
299
|
response = input(
|
190
300
|
" Do you still want to proceed with finishing? (y/n) "
|
191
301
|
)
|
@@ -210,34 +320,45 @@ def save_context_core(
|
|
210
320
|
logger.warning(
|
211
321
|
f"no html report found; to attach one, create an .html export for your {filepath.suffix} file and then run: lamin save {filepath}"
|
212
322
|
)
|
213
|
-
if report_path is not None and not from_cli:
|
214
|
-
|
215
|
-
|
216
|
-
|
323
|
+
if report_path is not None and is_r_notebook and not from_cli: # R notebooks
|
324
|
+
# see comment above in check_filepath_recently_saved
|
325
|
+
recently_saved_time = 0.3 if not is_retry else 20
|
326
|
+
if get_seconds_since_modified(report_path) > recently_saved_time:
|
327
|
+
# the automated retry solution of Jupyter notebooks does not work in RStudio because the execution of the notebook cell
|
328
|
+
# seems to block the event loop of the frontend
|
329
|
+
if not is_retry:
|
330
|
+
logger.warning(get_save_notebook_message_retry())
|
331
|
+
return "retry"
|
332
|
+
else:
|
333
|
+
logger.warning(
|
334
|
+
"the notebook on disk hasn't been saved within the last 20 sec"
|
335
|
+
)
|
336
|
+
save_source_code_and_report = False
|
217
337
|
ln.settings.creation.artifact_silence_missing_run_warning = True
|
218
|
-
#
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
338
|
+
# save source code
|
339
|
+
if save_source_code_and_report:
|
340
|
+
hash, _ = hash_file(source_code_path) # ignore hash_type for now
|
341
|
+
if transform.hash is not None:
|
342
|
+
# check if the hash of the transform source code matches
|
343
|
+
# (for scripts, we already run the same logic in track() - we can deduplicate the call at some point)
|
344
|
+
if hash != transform.hash:
|
345
|
+
response = input(
|
346
|
+
f"You are about to overwrite existing source code (hash '{transform.hash}') for Transform('{transform.uid}')."
|
347
|
+
f" Proceed? (y/n)"
|
348
|
+
)
|
349
|
+
if response == "y":
|
350
|
+
transform.source_code = source_code_path.read_text()
|
351
|
+
transform.hash = hash
|
352
|
+
else:
|
353
|
+
logger.warning("Please re-run `ln.track()` to make a new version")
|
354
|
+
return "rerun-the-notebook"
|
231
355
|
else:
|
232
|
-
logger.
|
233
|
-
return "rerun-the-notebook"
|
356
|
+
logger.debug("source code is already saved")
|
234
357
|
else:
|
235
|
-
|
236
|
-
|
237
|
-
transform.source_code = source_code_path.read_text()
|
238
|
-
transform.hash = hash
|
358
|
+
transform.source_code = source_code_path.read_text()
|
359
|
+
transform.hash = hash
|
239
360
|
|
240
|
-
# track environment
|
361
|
+
# track run environment
|
241
362
|
if run is not None:
|
242
363
|
env_path = ln_setup.settings.cache_dir / f"run_env_pip_{run.uid}.txt"
|
243
364
|
if env_path.exists():
|
@@ -250,7 +371,7 @@ def save_context_core(
|
|
250
371
|
artifact = ln.Artifact.filter(hash=hash, _branch_code=0).one_or_none()
|
251
372
|
new_env_artifact = artifact is None
|
252
373
|
if new_env_artifact:
|
253
|
-
artifact = ln.Artifact(
|
374
|
+
artifact = ln.Artifact( # type: ignore
|
254
375
|
env_path,
|
255
376
|
description="requirements.txt",
|
256
377
|
_branch_code=0,
|
@@ -263,50 +384,57 @@ def save_context_core(
|
|
263
384
|
|
264
385
|
# set finished_at
|
265
386
|
if finished_at and run is not None:
|
266
|
-
|
387
|
+
if not from_cli:
|
388
|
+
update_finished_at = True
|
389
|
+
else:
|
390
|
+
update_finished_at = run.finished_at is None
|
391
|
+
if update_finished_at:
|
392
|
+
run.finished_at = datetime.now(timezone.utc)
|
267
393
|
|
268
394
|
# track logs
|
269
395
|
if run is not None and not from_cli and not is_ipynb and not is_r_notebook:
|
270
396
|
save_run_logs(run)
|
271
397
|
|
272
398
|
# track report and set is_consecutive
|
273
|
-
if
|
274
|
-
if
|
275
|
-
if
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
399
|
+
if save_source_code_and_report:
|
400
|
+
if run is not None:
|
401
|
+
if report_path is not None:
|
402
|
+
if is_r_notebook:
|
403
|
+
title_text, report_path = clean_r_notebook_html(report_path)
|
404
|
+
if title_text is not None:
|
405
|
+
transform.description = title_text
|
406
|
+
if run.report_id is not None:
|
407
|
+
hash, _ = hash_file(report_path) # ignore hash_type for now
|
408
|
+
if hash != run.report.hash:
|
409
|
+
response = input(
|
410
|
+
f"You are about to overwrite an existing report (hash '{run.report.hash}') for Run('{run.uid}'). Proceed? (y/n)"
|
411
|
+
)
|
412
|
+
if response == "y":
|
413
|
+
run.report.replace(report_path)
|
414
|
+
run.report.save(upload=True, print_progress=False)
|
415
|
+
else:
|
416
|
+
logger.important("keeping old report")
|
288
417
|
else:
|
289
|
-
logger.important("
|
418
|
+
logger.important("report is already saved")
|
290
419
|
else:
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
420
|
+
report_file = ln.Artifact( # type: ignore
|
421
|
+
report_path,
|
422
|
+
description=f"Report of run {run.uid}",
|
423
|
+
_branch_code=0, # hidden file
|
424
|
+
run=False,
|
425
|
+
)
|
426
|
+
report_file.save(upload=True, print_progress=False)
|
427
|
+
run.report = report_file
|
428
|
+
if is_r_notebook:
|
429
|
+
# this is the "cleaned" report
|
430
|
+
report_path.unlink()
|
431
|
+
logger.debug(
|
432
|
+
f"saved transform.latest_run.report: {transform.latest_run.report}"
|
298
433
|
)
|
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
|
434
|
+
run._is_consecutive = is_consecutive
|
308
435
|
|
309
|
-
|
436
|
+
# save both run & transform records if we arrive here
|
437
|
+
if run is not None:
|
310
438
|
run.save()
|
311
439
|
transform.save()
|
312
440
|
|
@@ -318,19 +446,32 @@ def save_context_core(
|
|
318
446
|
hours = seconds // 3600
|
319
447
|
minutes = (seconds % 3600) // 60
|
320
448
|
secs = seconds % 60
|
321
|
-
formatted_run_time =
|
449
|
+
formatted_run_time = (
|
450
|
+
f"{days}d"
|
451
|
+
if days != 0
|
452
|
+
else "" + f"{hours}h"
|
453
|
+
if hours != 0
|
454
|
+
else "" + f"{minutes}m"
|
455
|
+
if minutes != 0
|
456
|
+
else "" + f"{secs}s"
|
457
|
+
)
|
322
458
|
|
323
459
|
logger.important(
|
324
460
|
f"finished Run('{run.uid[:8]}') after {formatted_run_time} at {format_field_value(run.finished_at)}"
|
325
461
|
)
|
326
462
|
if ln_setup.settings.instance.is_on_hub:
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
463
|
+
instance_slug = ln_setup.settings.instance.slug
|
464
|
+
if save_source_code_and_report:
|
465
|
+
logger.important(
|
466
|
+
f"go to: https://lamin.ai/{instance_slug}/transform/{transform.uid}"
|
467
|
+
)
|
468
|
+
if not from_cli and save_source_code_and_report:
|
332
469
|
thing = "notebook" if (is_ipynb or is_r_notebook) else "script"
|
333
470
|
logger.important(
|
334
|
-
f"
|
471
|
+
f"to update your {thing} from the CLI, run: lamin save {filepath}"
|
335
472
|
)
|
473
|
+
if not save_source_code_and_report:
|
474
|
+
logger.warning(
|
475
|
+
f"did *not* save source code and report -- to do so, run: lamin save {filepath}"
|
476
|
+
)
|
336
477
|
return None
|