lamindb 0.76.9__py3-none-any.whl → 0.76.11__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 +15 -14
- lamindb/_artifact.py +2 -1
- lamindb/_can_validate.py +46 -4
- lamindb/_collection.py +2 -1
- lamindb/_curate.py +3 -1
- lamindb/_feature_set.py +3 -1
- lamindb/_finish.py +19 -18
- lamindb/_from_values.py +110 -89
- lamindb/_query_set.py +3 -1
- lamindb/_record.py +81 -62
- lamindb/_run.py +3 -0
- lamindb/_save.py +3 -1
- lamindb/_transform.py +9 -6
- lamindb/core/_context.py +94 -78
- lamindb/core/_data.py +113 -41
- lamindb/core/_django.py +209 -0
- lamindb/core/_feature_manager.py +140 -13
- lamindb/core/_label_manager.py +58 -23
- lamindb/core/_mapped_collection.py +1 -1
- lamindb/core/_settings.py +2 -1
- lamindb/core/exceptions.py +9 -9
- lamindb/core/storage/_anndata_accessor.py +2 -1
- lamindb/core/versioning.py +2 -14
- {lamindb-0.76.9.dist-info → lamindb-0.76.11.dist-info}/METADATA +8 -8
- {lamindb-0.76.9.dist-info → lamindb-0.76.11.dist-info}/RECORD +27 -26
- {lamindb-0.76.9.dist-info → lamindb-0.76.11.dist-info}/LICENSE +0 -0
- {lamindb-0.76.9.dist-info → lamindb-0.76.11.dist-info}/WHEEL +0 -0
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
|
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
|
-
|
93
|
-
|
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}
|
91
|
+
print(f"→ {message}")
|
98
92
|
response = input("→ Ready to re-run? (y/n)")
|
99
93
|
if response == "y":
|
100
94
|
logger.important(
|
101
|
-
"
|
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.
|
133
|
-
>>> # do things
|
134
|
-
>>> ln.
|
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
|
-
"""
|
144
|
+
"""Managed transform of context."""
|
151
145
|
return self._transform
|
152
146
|
|
153
147
|
@property
|
154
148
|
def uid(self) -> str | None:
|
155
|
-
"""`uid`
|
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
|
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`
|
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
|
-
"""
|
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
|
-
"""
|
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.
|
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.
|
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(
|
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(
|
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(
|
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
|
394
|
+
raise NotebookNotSaved(
|
396
395
|
"The notebook is not saved, please save the notebook and"
|
397
|
-
" rerun
|
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
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
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
|
-
|
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
|
-
|
475
|
-
|
476
|
-
|
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(
|
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.
|
523
|
+
f'ln.track("{uid[:-4]}{increment_base62(uid[-4:])}")'
|
508
524
|
)
|
509
525
|
else:
|
510
|
-
self._logging_message += f"loaded Transform(
|
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
|
-
"""
|
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.
|
547
|
+
>>> ln.track()
|
532
548
|
>>> # do things while tracking data lineage
|
533
|
-
>>> ln.
|
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.
|
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.
|
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
|
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.
|
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
|
-
#
|
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
|
-
#
|
176
|
-
msg +=
|
239
|
+
# Labels and features
|
240
|
+
msg += format_labels_and_features(self, {}, print_types)
|
177
241
|
|
178
|
-
#
|
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,
|
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
|
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":
|