lamindb 0.76.9__py3-none-any.whl → 0.76.10__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/core/_context.py CHANGED
@@ -12,15 +12,14 @@ from lamindb_setup.core.hashing import hash_file
12
12
  from lnschema_core import Run, Transform, ids
13
13
  from lnschema_core.ids import base62_12
14
14
  from lnschema_core.models import format_field_value
15
- from lnschema_core.users import current_user_id
16
15
 
17
16
  from ._settings import settings
18
17
  from ._sync_git import get_transform_reference_from_git_repo
19
18
  from ._track_environment import track_environment
20
19
  from .exceptions import (
20
+ InconsistentKey,
21
21
  MissingContextUID,
22
22
  NotebookNotSaved,
23
- NotebookNotSavedError,
24
23
  NoTitleError,
25
24
  TrackNotCalled,
26
25
  UpdateContext,
@@ -35,9 +34,7 @@ if TYPE_CHECKING:
35
34
 
36
35
  is_run_from_ipython = getattr(builtins, "__IPYTHON__", False)
37
36
 
38
- msg_path_failed = (
39
- "failed to infer notebook path.\nfix: pass `path` to ln.context.track()"
40
- )
37
+ msg_path_failed = "failed to infer notebook path.\nfix: pass `path` to `ln.track()`"
41
38
 
42
39
 
43
40
  def get_uid_ext(version: str) -> str:
@@ -85,25 +82,22 @@ def raise_missing_context(transform_type: str, key: str) -> bool:
85
82
  transform = Transform.filter(key=key).latest_version().first()
86
83
  if transform is None:
87
84
  new_uid = f"{base62_12()}0000"
88
- message = f"To track this {transform_type}, copy & paste the below into the current cell and re-run it\n\n"
89
- message += f'ln.context.uid = "{new_uid}"\nln.context.track()'
85
+ message = f'to track this {transform_type}, copy & paste `ln.track("{new_uid}")` and re-run'
90
86
  else:
91
87
  uid = transform.uid
92
- suid, vuid = uid[: Transform._len_stem_uid], uid[Transform._len_stem_uid :]
93
- new_vuid = increment_base62(vuid)
94
- new_uid = f"{suid}{new_vuid}"
95
- message = f"You already have a version family with key '{key}' (stem_uid='{transform.stem_uid}').\n\n- to make a revision, set `ln.context.uid = '{new_uid}'`\n- to start a new version family, rename your file and rerun: `ln.context.track()`"
88
+ new_uid = f"{uid[:-4]}{increment_base62(uid[-4:])}"
89
+ message = f"you already have a transform with key '{key}' ('{transform.uid}')\n - to make a revision, call `ln.track('{new_uid}')`\n - to create a new transform, rename your file and run: `ln.track()`"
96
90
  if transform_type == "notebook":
97
- print(f"→ {message}\n")
91
+ print(f"→ {message}")
98
92
  response = input("→ Ready to re-run? (y/n)")
99
93
  if response == "y":
100
94
  logger.important(
101
- "Note: Restart your notebook if you want consecutive cell execution"
95
+ "note: restart your notebook if you want consecutive cell execution"
102
96
  )
103
97
  return True
104
98
  raise MissingContextUID("Please follow the instructions.")
105
99
  else:
106
- raise MissingContextUID(message)
100
+ raise MissingContextUID(f"✗ {message}")
107
101
  return False
108
102
 
109
103
 
@@ -126,12 +120,12 @@ class Context:
126
120
 
127
121
  Examples:
128
122
 
129
- Is typically used via :class:`~lamindb.context`:
123
+ Is typically used via the global :class:`~lamindb.context` object via `ln.track()` and `ln.finish()`:
130
124
 
131
125
  >>> import lamindb as ln
132
- >>> ln.context.track()
133
- >>> # do things while tracking data lineage
134
- >>> ln.context.finish()
126
+ >>> ln.track()
127
+ >>> # do things
128
+ >>> ln.finish()
135
129
 
136
130
  """
137
131
 
@@ -147,12 +141,12 @@ class Context:
147
141
 
148
142
  @property
149
143
  def transform(self) -> Transform | None:
150
- """Transform of context."""
144
+ """Managed transform of context."""
151
145
  return self._transform
152
146
 
153
147
  @property
154
148
  def uid(self) -> str | None:
155
- """`uid` to create transform."""
149
+ """`uid` argument for `context.transform`."""
156
150
  return self._uid
157
151
 
158
152
  @uid.setter
@@ -161,7 +155,7 @@ class Context:
161
155
 
162
156
  @property
163
157
  def name(self) -> str | None:
164
- """`name` to create transform."""
158
+ """`name argument for `context.transform`."""
165
159
  return self._name
166
160
 
167
161
  @name.setter
@@ -170,7 +164,7 @@ class Context:
170
164
 
171
165
  @property
172
166
  def version(self) -> str | None:
173
- """`version` to create transform."""
167
+ """`version` argument for `context.transform`."""
174
168
  return self._version
175
169
 
176
170
  @version.setter
@@ -179,18 +173,19 @@ class Context:
179
173
 
180
174
  @property
181
175
  def run(self) -> Run | None:
182
- """Run of context."""
176
+ """Managed run of context."""
183
177
  return self._run
184
178
 
185
179
  def track(
186
180
  self,
181
+ uid: str | None = None,
187
182
  *,
188
183
  params: dict | None = None,
189
184
  new_run: bool | None = None,
190
185
  path: str | None = None,
191
186
  transform: Transform | None = None,
192
187
  ) -> None:
193
- """Starts data lineage tracking for a run.
188
+ """Initiate a run with tracked data lineage.
194
189
 
195
190
  - sets :attr:`~lamindb.core.Context.transform` &
196
191
  :attr:`~lamindb.core.Context.run` by creating or loading `Transform` &
@@ -201,6 +196,7 @@ class Context:
201
196
  script-like transform exists in a git repository and links it.
202
197
 
203
198
  Args:
199
+ uid: A `uid` to create or load a transform.
204
200
  params: A dictionary of parameters to track for the run.
205
201
  new_run: If `False`, loads latest run of transform
206
202
  (default notebook), if `True`, creates new run (default pipeline).
@@ -213,9 +209,11 @@ class Context:
213
209
  To track the run of a notebook or script, call:
214
210
 
215
211
  >>> import lamindb as ln
216
- >>> ln.context.track()
212
+ >>> ln.track()
217
213
 
218
214
  """
215
+ if uid is not None:
216
+ self.uid = uid
219
217
  self._path = None
220
218
  if transform is None:
221
219
  is_tracked = False
@@ -225,7 +223,7 @@ class Context:
225
223
  )
226
224
  transform = None
227
225
  stem_uid = None
228
- if self.uid is not None:
226
+ if uid is not None or self.uid is not None:
229
227
  transform = Transform.filter(uid=self.uid).one_or_none()
230
228
  if self.version is not None:
231
229
  # test inconsistent version passed
@@ -295,7 +293,7 @@ class Context:
295
293
  else:
296
294
  if transform.type in {"notebook", "script"}:
297
295
  raise ValueError(
298
- "Use ln.context.track() without passing transform in a notebook or script"
296
+ "Use `ln.track()` without passing transform in a notebook or script"
299
297
  " - metadata is automatically parsed"
300
298
  )
301
299
  transform_exists = None
@@ -304,10 +302,10 @@ class Context:
304
302
  transform_exists = Transform.filter(id=transform.id).first()
305
303
  if transform_exists is None:
306
304
  transform.save()
307
- self._logging_message += f"created Transform(uid='{transform.uid}')"
305
+ self._logging_message += f"created Transform('{transform.uid[:8]}')"
308
306
  transform_exists = transform
309
307
  else:
310
- self._logging_message += f"loaded Transform(uid='{transform.uid}')"
308
+ self._logging_message += f"loaded Transform('{transform.uid[:8]}')"
311
309
  self._transform = transform_exists
312
310
 
313
311
  if new_run is None: # for notebooks, default to loading latest runs
@@ -316,15 +314,15 @@ class Context:
316
314
  run = None
317
315
  if not new_run: # try loading latest run by same user
318
316
  run = (
319
- Run.filter(transform=self._transform, created_by_id=current_user_id())
317
+ Run.filter(
318
+ transform=self._transform, created_by_id=ln_setup.settings.user.id
319
+ )
320
320
  .order_by("-created_at")
321
321
  .first()
322
322
  )
323
323
  if run is not None: # loaded latest run
324
324
  run.started_at = datetime.now(timezone.utc) # update run time
325
- self._logging_message += (
326
- f" & loaded Run(started_at={format_field_value(run.started_at)})"
327
- )
325
+ self._logging_message += f", started Run('{run.uid[:8]}') at {format_field_value(run.started_at)}"
328
326
 
329
327
  if run is None: # create new run
330
328
  run = Run(
@@ -332,9 +330,7 @@ class Context:
332
330
  params=params,
333
331
  )
334
332
  run.started_at = datetime.now(timezone.utc)
335
- self._logging_message += (
336
- f" & created Run(started_at={format_field_value(run.started_at)})"
337
- )
333
+ self._logging_message += f", started new Run('{run.uid[:8]}') at {format_field_value(run.started_at)}"
338
334
  # can only determine at ln.finish() if run was consecutive in
339
335
  # interactive session, otherwise, is consecutive
340
336
  run.is_consecutive = True if is_run_from_ipython else None
@@ -342,6 +338,9 @@ class Context:
342
338
  run.save()
343
339
  if params is not None:
344
340
  run.params.add_values(params)
341
+ self._logging_message += "\n→ params: " + " ".join(
342
+ f"{key}='{value}'" for key, value in params.items()
343
+ )
345
344
  self._run = run
346
345
  track_environment(run)
347
346
  logger.important(self._logging_message)
@@ -392,9 +391,9 @@ class Context:
392
391
  try:
393
392
  nbproject_title = nbproject.meta.live.title
394
393
  except IndexError:
395
- raise NotebookNotSavedError(
394
+ raise NotebookNotSaved(
396
395
  "The notebook is not saved, please save the notebook and"
397
- " rerun `ln.context.track()`"
396
+ " rerun ``"
398
397
  ) from None
399
398
  if nbproject_title is None:
400
399
  raise NoTitleError(
@@ -430,52 +429,74 @@ class Context:
430
429
  transform_type: TransformType = None,
431
430
  transform: Transform | None = None,
432
431
  ):
432
+ def get_key_clashing_message(transform: Transform, key: str) -> str:
433
+ update_key_note = message_update_key_in_version_family(
434
+ suid=transform.stem_uid,
435
+ existing_key=transform.key,
436
+ new_key=key,
437
+ registry="Transform",
438
+ )
439
+ return (
440
+ f'Filename "{key}" clashes with the existing key "{transform.key}" for uid "{transform.uid[:-4]}...."\n\nEither init a new transform with a new uid:\n\n'
441
+ f'ln.track("{ids.base62_12()}0000)"\n\n{update_key_note}'
442
+ )
443
+
433
444
  # make a new transform record
434
445
  if transform is None:
435
446
  if uid is None:
436
447
  uid = f"{stem_uid}{get_uid_ext(version)}"
448
+ # let's query revises so that we can pass it to the constructor and use it for error handling
449
+ revises = (
450
+ Transform.filter(uid__startswith=uid[:-4], is_latest=True)
451
+ .order_by("-created_at")
452
+ .first()
453
+ )
437
454
  # note that here we're not passing revises because we're not querying it
438
455
  # hence, we need to do a revision family lookup based on key
439
456
  # hence, we need key to be not None
440
457
  assert key is not None # noqa: S101
441
- transform = Transform(
442
- uid=uid,
443
- version=version,
444
- name=name,
445
- key=key,
446
- reference=transform_ref,
447
- reference_type=transform_ref_type,
448
- type=transform_type,
449
- ).save()
450
- self._logging_message += f"created Transform(uid='{transform.uid}')"
458
+ raise_update_context = False
459
+ try:
460
+ transform = Transform(
461
+ uid=uid,
462
+ version=version,
463
+ name=name,
464
+ key=key,
465
+ reference=transform_ref,
466
+ reference_type=transform_ref_type,
467
+ type=transform_type,
468
+ revises=revises,
469
+ ).save()
470
+ except InconsistentKey:
471
+ raise_update_context = True
472
+ if raise_update_context:
473
+ raise UpdateContext(get_key_clashing_message(revises, key))
474
+ self._logging_message += f"created Transform('{transform.uid[:8]}')"
451
475
  else:
452
476
  uid = transform.uid
477
+ # transform was already saved via `finish()`
478
+ transform_was_saved = (
479
+ transform._source_code_artifact_id is not None
480
+ or transform.source_code is not None
481
+ )
453
482
  # check whether the transform.key is consistent
454
483
  if transform.key != key:
455
- suid = transform.stem_uid
456
- new_suid = ids.base62_12()
457
- transform_type = "notebook" if is_run_from_ipython else "script"
458
- note = message_update_key_in_version_family(
459
- suid=suid,
460
- existing_key=transform.key,
461
- new_key=key,
462
- registry="Transform",
463
- )
464
- raise UpdateContext(
465
- f'\n✗ Filename "{key}" clashes with the existing key "{transform.key}" for uid "{transform.uid[:-4]}...."\n\nEither init a new transform with a new uid:\n\n'
466
- f'ln.context.uid = "{new_suid}0000"\n\n{note}'
467
- )
484
+ raise UpdateContext(get_key_clashing_message(transform, key))
468
485
  elif transform.name != name:
469
486
  transform.name = name
470
487
  transform.save()
471
488
  self._logging_message += (
472
489
  "updated transform name, " # white space on purpose
473
490
  )
474
- # check whether transform source code was already saved
475
- if (
476
- transform._source_code_artifact_id is not None
477
- or transform.source_code is not None
491
+ elif (
492
+ transform.created_by_id != ln_setup.settings.user.id
493
+ and not transform_was_saved
478
494
  ):
495
+ raise UpdateContext(
496
+ 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* filename and `ln.track("{ids.base62_12()}0000")`.'
497
+ )
498
+ # check whether transform source code was already saved
499
+ if transform_was_saved:
479
500
  bump_revision = False
480
501
  if is_run_from_ipython:
481
502
  bump_revision = True
@@ -489,7 +510,7 @@ class Context:
489
510
  bump_revision = True
490
511
  else:
491
512
  self._logging_message += (
492
- f"loaded Transform(uid='{transform.uid}')"
513
+ f"loaded Transform('{transform.uid[:8]}')"
493
514
  )
494
515
  if bump_revision:
495
516
  change_type = (
@@ -497,21 +518,16 @@ class Context:
497
518
  if is_run_from_ipython
498
519
  else "Source code changed"
499
520
  )
500
- suid, vuid = (
501
- uid[:-4],
502
- uid[-4:],
503
- )
504
- new_vuid = increment_base62(vuid)
505
521
  raise UpdateContext(
506
522
  f"{change_type}, bump revision by setting:\n\n"
507
- f'ln.context.uid = "{suid}{new_vuid}"'
523
+ f'ln.track("{uid[:-4]}{increment_base62(uid[-4:])}")'
508
524
  )
509
525
  else:
510
- self._logging_message += f"loaded Transform(uid='{transform.uid}')"
526
+ self._logging_message += f"loaded Transform('{transform.uid[:8]}')"
511
527
  self._transform = transform
512
528
 
513
529
  def finish(self, ignore_non_consecutive: None | bool = None) -> None:
514
- """Mark the run context as finished.
530
+ """Finish a tracked run.
515
531
 
516
532
  - writes a timestamp: `run.finished_at`
517
533
  - saves the source code: `transform.source_code`
@@ -528,9 +544,9 @@ class Context:
528
544
  Examples:
529
545
 
530
546
  >>> import lamindb as ln
531
- >>> ln.context.track()
547
+ >>> ln.track()
532
548
  >>> # do things while tracking data lineage
533
- >>> ln.context.finish()
549
+ >>> ln.finish()
534
550
 
535
551
  See Also:
536
552
  `lamin save script.py` or `lamin save notebook.ipynb` → `docs </cli#lamin-save>`__
@@ -547,7 +563,7 @@ class Context:
547
563
  return "CMD + s" if platform.system() == "Darwin" else "CTRL + s"
548
564
 
549
565
  if context.run is None:
550
- raise TrackNotCalled("Please run `ln.context.track()` before `ln.finish()`")
566
+ raise TrackNotCalled("Please run `ln.track()` before `ln.finish()`")
551
567
  if context._path is None:
552
568
  if context.run.transform.type in {"script", "notebook"}:
553
569
  raise ValueError(
@@ -560,7 +576,7 @@ class Context:
560
576
  if is_run_from_ipython: # notebooks
561
577
  if get_seconds_since_modified(context._path) > 2 and not ln_setup._TESTING:
562
578
  raise NotebookNotSaved(
563
- f"Please save the notebook in your editor (shortcut `{get_shortcut()}`) right before calling `ln.context.finish()`"
579
+ f"Please save the notebook in your editor (shortcut `{get_shortcut()}`) right before calling `ln.finish()`"
564
580
  )
565
581
  save_context_core(
566
582
  run=context.run,
lamindb/core/_data.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections import defaultdict
4
- from typing import TYPE_CHECKING, Any, Iterable, List
4
+ from typing import TYPE_CHECKING, Any
5
5
 
6
+ from django.db import connections
6
7
  from lamin_utils import colors, logger
7
8
  from lamindb_setup.core._docs import doc_args
8
9
  from lnschema_core.models import (
@@ -23,6 +24,7 @@ from lamindb._record import get_name_field
23
24
  from lamindb.core._settings import settings
24
25
 
25
26
  from ._context import context
27
+ from ._django import get_artifact_with_related, get_related_model
26
28
  from ._feature_manager import (
27
29
  get_feature_set_links,
28
30
  get_host_id_field,
@@ -37,14 +39,14 @@ from .schema import (
37
39
  )
38
40
 
39
41
  if TYPE_CHECKING:
42
+ from collections.abc import Iterable
43
+
40
44
  from lnschema_core.types import StrField
41
45
 
42
46
 
43
- WARNING_RUN_TRANSFORM = (
44
- "no run & transform got linked, call `ln.context.track()` & re-run"
45
- )
47
+ WARNING_RUN_TRANSFORM = "no run & transform got linked, call `ln.track()` & re-run"
46
48
 
47
- WARNING_NO_INPUT = "run input wasn't tracked, call `ln.context.track()` and re-run"
49
+ WARNING_NO_INPUT = "run input wasn't tracked, call `ln.track()` and re-run"
48
50
 
49
51
 
50
52
  def get_run(run: Run | None) -> Run | None:
@@ -98,23 +100,90 @@ def save_feature_set_links(self: Artifact | Collection) -> None:
98
100
  bulk_create(links, ignore_conflicts=True)
99
101
 
100
102
 
103
+ def format_provenance(self, fk_data, print_types):
104
+ type_str = lambda attr: (
105
+ f": {get_related_model(self.__class__, attr).__name__}" if print_types else ""
106
+ )
107
+
108
+ return "".join(
109
+ [
110
+ f" .{field_name}{type_str(field_name)} = {format_field_value(value.get('name'))}\n"
111
+ for field_name, value in fk_data.items()
112
+ if value.get("name")
113
+ ]
114
+ )
115
+
116
+
117
+ def format_input_of_runs(self, print_types):
118
+ if self.id is not None and self.input_of_runs.exists():
119
+ values = [format_field_value(i.started_at) for i in self.input_of_runs.all()]
120
+ type_str = ": Run" if print_types else "" # type: ignore
121
+ return f" .input_of_runs{type_str} = {', '.join(values)}\n"
122
+ return ""
123
+
124
+
125
+ def format_labels_and_features(self, related_data, print_types):
126
+ msg = print_labels(
127
+ self, m2m_data=related_data.get("m2m", {}), print_types=print_types
128
+ )
129
+ if isinstance(self, Artifact):
130
+ msg += print_features( # type: ignore
131
+ self,
132
+ related_data=related_data,
133
+ print_types=print_types,
134
+ print_params=hasattr(self, "type") and self.type == "model",
135
+ )
136
+ return msg
137
+
138
+
139
+ def _describe_postgres(self: Artifact | Collection, print_types: bool = False):
140
+ model_name = self.__class__.__name__
141
+ msg = f"{colors.green(model_name)}{record_repr(self, include_foreign_keys=False).lstrip(model_name)}\n"
142
+ if self._state.db is not None and self._state.db != "default":
143
+ msg += f" {colors.italic('Database instance')}\n"
144
+ msg += f" slug: {self._state.db}\n"
145
+
146
+ if model_name == "Artifact":
147
+ result = get_artifact_with_related(
148
+ self,
149
+ include_feature_link=True,
150
+ include_fk=True,
151
+ include_m2m=True,
152
+ include_featureset=True,
153
+ )
154
+ else:
155
+ result = get_artifact_with_related(self, include_fk=True, include_m2m=True)
156
+ related_data = result.get("related_data", {})
157
+ fk_data = related_data.get("fk", {})
158
+
159
+ # Provenance
160
+ prov_msg = format_provenance(self, fk_data, print_types)
161
+ if prov_msg:
162
+ msg += f" {colors.italic('Provenance')}\n{prov_msg}"
163
+
164
+ # Input of runs
165
+ input_of_message = format_input_of_runs(self, print_types)
166
+ if input_of_message:
167
+ msg += f" {colors.italic('Usage')}\n{input_of_message}"
168
+
169
+ # Labels and features
170
+ msg += format_labels_and_features(self, related_data, print_types)
171
+
172
+ # Print entire message
173
+ logger.print(msg)
174
+
175
+
101
176
  @doc_args(Artifact.describe.__doc__)
102
- def describe(self: Artifact, print_types: bool = False):
177
+ def describe(self: Artifact | Collection, print_types: bool = False):
103
178
  """{}""" # noqa: D415
179
+ if not self._state.adding and connections[self._state.db].vendor == "postgresql":
180
+ return _describe_postgres(self, print_types=print_types)
181
+
104
182
  model_name = self.__class__.__name__
105
183
  msg = f"{colors.green(model_name)}{record_repr(self, include_foreign_keys=False).lstrip(model_name)}\n"
106
184
  if self._state.db is not None and self._state.db != "default":
107
185
  msg += f" {colors.italic('Database instance')}\n"
108
186
  msg += f" slug: {self._state.db}\n"
109
- # prefetch all many-to-many relationships
110
- # doesn't work for describing using artifact
111
- # self = (
112
- # self.__class__.objects.using(self._state.db)
113
- # .prefetch_related(
114
- # *[f.name for f in self.__class__._meta.get_fields() if f.many_to_many]
115
- # )
116
- # .get(id=self.id)
117
- # )
118
187
 
119
188
  prov_msg = ""
120
189
  fields = self._meta.fields
@@ -162,28 +231,15 @@ def describe(self: Artifact, print_types: bool = False):
162
231
  msg += f" {colors.italic('Provenance')}\n"
163
232
  msg += prov_msg
164
233
 
165
- # input of runs
166
- input_of_message = ""
167
- if self.id is not None and self.input_of_runs.exists():
168
- values = [format_field_value(i.started_at) for i in self.input_of_runs.all()]
169
- type_str = ": Run" if print_types else "" # type: ignore
170
- input_of_message += f" .input_of_runs{type_str} = {', '.join(values)}\n"
234
+ # Input of runs
235
+ input_of_message = format_input_of_runs(self, print_types)
171
236
  if input_of_message:
172
- msg += f" {colors.italic('Usage')}\n"
173
- msg += input_of_message
237
+ msg += f" {colors.italic('Usage')}\n{input_of_message}"
174
238
 
175
- # labels
176
- msg += print_labels(self, print_types=print_types)
239
+ # Labels and features
240
+ msg += format_labels_and_features(self, {}, print_types)
177
241
 
178
- # features
179
- if isinstance(self, Artifact):
180
- msg += print_features( # type: ignore
181
- self,
182
- print_types=print_types,
183
- print_params=hasattr(self, "type") and self.type == "model",
184
- )
185
-
186
- # print entire message
242
+ # Print entire message
187
243
  logger.print(msg)
188
244
 
189
245
 
@@ -258,7 +314,7 @@ def add_labels(
258
314
  records = records.list()
259
315
  if isinstance(records, (str, Record)):
260
316
  records = [records]
261
- if not isinstance(records, List): # avoids warning for pd Series
317
+ if not isinstance(records, list): # avoids warning for pd Series
262
318
  records = list(records)
263
319
  # create records from values
264
320
  if len(records) == 0:
@@ -369,10 +425,29 @@ def _track_run_input(
369
425
  if run is not None:
370
426
  # avoid cycles: data can't be both input and output
371
427
  def is_valid_input(data: Artifact | Collection):
428
+ is_valid = False
429
+ if data._state.db == "default":
430
+ # things are OK if the record is on the default db
431
+ is_valid = True
432
+ elif data._state.db is None:
433
+ # if a record is not yet saved, it can't be an input
434
+ # we silently ignore because what likely happens is that
435
+ # the user works with an object that's about to be saved
436
+ # in the current Python session
437
+ is_valid = False
438
+ else:
439
+ # record is on another db
440
+ # we have to save the record into the current db with
441
+ # the run being attached to a transfer transform
442
+ logger.important(
443
+ f"completing transfer to track {data.__class__.__name__}('{data.uid[:8]}') as input"
444
+ )
445
+ data.save()
446
+ is_valid = True
372
447
  return (
373
448
  data.run_id != run.id
374
- and not data._state.adding
375
- and data._state.db in {"default", None}
449
+ and not data._state.adding # this seems duplicated with data._state.db is None
450
+ and is_valid
376
451
  )
377
452
 
378
453
  input_data = [data for data in data_iter if is_valid_input(data)]
@@ -413,10 +488,7 @@ def _track_run_input(
413
488
  track_run_input = is_run_input
414
489
  if track_run_input:
415
490
  if run is None:
416
- raise ValueError(
417
- "No run context set. Call ln.context.track() or link input to a"
418
- " run object via `run.input_artifacts.add(artifact)`"
419
- )
491
+ raise ValueError("No run context set. Call `ln.track()`.")
420
492
  # avoid adding the same run twice
421
493
  run.save()
422
494
  if data_class_name == "artifact":