lamindb 0.76.12__py3-none-any.whl → 0.76.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lamindb/__init__.py +1 -1
- lamindb/_artifact.py +8 -9
- lamindb/_collection.py +18 -5
- lamindb/_curate.py +242 -137
- lamindb/_feature_set.py +3 -1
- lamindb/_from_values.py +1 -5
- lamindb/_parents.py +18 -3
- lamindb/_query_manager.py +0 -15
- lamindb/_query_set.py +8 -4
- lamindb/_record.py +82 -6
- lamindb/core/__init__.py +2 -0
- lamindb/core/_context.py +1 -1
- lamindb/core/_data.py +19 -7
- lamindb/core/_django.py +19 -5
- lamindb/core/_feature_manager.py +80 -44
- lamindb/core/_label_manager.py +91 -93
- lamindb/core/exceptions.py +7 -0
- lamindb/core/schema.py +42 -3
- lamindb/core/types.py +1 -0
- {lamindb-0.76.12.dist-info → lamindb-0.76.14.dist-info}/METADATA +6 -6
- {lamindb-0.76.12.dist-info → lamindb-0.76.14.dist-info}/RECORD +23 -23
- {lamindb-0.76.12.dist-info → lamindb-0.76.14.dist-info}/LICENSE +0 -0
- {lamindb-0.76.12.dist-info → lamindb-0.76.14.dist-info}/WHEEL +0 -0
lamindb/_feature_set.py
CHANGED
@@ -216,7 +216,9 @@ def members(self) -> QuerySet:
|
|
216
216
|
|
217
217
|
|
218
218
|
def _get_related_name(self: FeatureSet) -> str:
|
219
|
-
feature_sets_related_models = dict_related_model_to_related_name(
|
219
|
+
feature_sets_related_models = dict_related_model_to_related_name(
|
220
|
+
self, instance=self._state.db
|
221
|
+
)
|
220
222
|
related_name = feature_sets_related_models.get(self.registry)
|
221
223
|
return related_name
|
222
224
|
|
lamindb/_from_values.py
CHANGED
@@ -64,11 +64,7 @@ def get_or_create_records(
|
|
64
64
|
if source_record:
|
65
65
|
from bionty.core._add_ontology import check_source_in_db
|
66
66
|
|
67
|
-
check_source_in_db(
|
68
|
-
registry=registry,
|
69
|
-
source=source_record,
|
70
|
-
update=True,
|
71
|
-
)
|
67
|
+
check_source_in_db(registry=registry, source=source_record)
|
72
68
|
|
73
69
|
from_source = not source_record.in_db
|
74
70
|
elif hasattr(registry, "source_id"):
|
lamindb/_parents.py
CHANGED
@@ -310,7 +310,12 @@ def _record_label(record: Record, field: str | None = None):
|
|
310
310
|
rf' FACE="Monospace">uid={record.uid}<BR/>version={record.version}</FONT>>'
|
311
311
|
)
|
312
312
|
elif isinstance(record, Run):
|
313
|
-
|
313
|
+
if record.transform.name:
|
314
|
+
name = f'{record.transform.name.replace("&", "&")}'
|
315
|
+
elif record.transform.key:
|
316
|
+
name = f'{record.transform.key.replace("&", "&")}'
|
317
|
+
else:
|
318
|
+
name = f"{record.transform.uid}"
|
314
319
|
user_display = (
|
315
320
|
record.created_by.handle
|
316
321
|
if record.created_by.name is None
|
@@ -365,7 +370,6 @@ def _get_all_parent_runs(data: Artifact | Collection) -> list:
|
|
365
370
|
inputs_run += (
|
366
371
|
r.input_collections.all().filter(visibility__in=[0, 1]).list()
|
367
372
|
)
|
368
|
-
run_inputs_outputs += [(inputs_run, r)]
|
369
373
|
outputs_run = (
|
370
374
|
r.__getattribute__(f"output_{name}s")
|
371
375
|
.all()
|
@@ -376,7 +380,18 @@ def _get_all_parent_runs(data: Artifact | Collection) -> list:
|
|
376
380
|
outputs_run += (
|
377
381
|
r.output_collections.all().filter(visibility__in=[0, 1]).list()
|
378
382
|
)
|
379
|
-
|
383
|
+
# if inputs are outputs artifacts are the same, will result infinite loop
|
384
|
+
# so only show as outputs
|
385
|
+
overlap = set(inputs_run).intersection(outputs_run)
|
386
|
+
if overlap:
|
387
|
+
logger.warning(
|
388
|
+
f"The following artifacts are both inputs and outputs of Run(uid={r.uid}): {overlap}\n → Only showing as outputs."
|
389
|
+
)
|
390
|
+
inputs_run = list(set(inputs_run) - overlap)
|
391
|
+
if len(inputs_run) > 0:
|
392
|
+
run_inputs_outputs += [(inputs_run, r)]
|
393
|
+
if len(outputs_run) > 0:
|
394
|
+
run_inputs_outputs += [(r, outputs_run)]
|
380
395
|
inputs += inputs_run
|
381
396
|
runs = [f.run for f in inputs if f.run is not None]
|
382
397
|
return run_inputs_outputs
|
lamindb/_query_manager.py
CHANGED
@@ -98,26 +98,11 @@ class QueryManager(models.Manager):
|
|
98
98
|
|
99
99
|
return _lookup(cls=self.all(), field=field, **kwargs)
|
100
100
|
|
101
|
-
def __getitem__(self, item: str):
|
102
|
-
try:
|
103
|
-
source_field_name = self.source_field_name
|
104
|
-
target_field_name = self.target_field_name
|
105
|
-
|
106
|
-
if (
|
107
|
-
source_field_name in {"artifact", "collection"}
|
108
|
-
and target_field_name == "feature_set"
|
109
|
-
):
|
110
|
-
return get_feature_set_by_slot_(host=self.instance).get(item)
|
111
|
-
|
112
|
-
except Exception: # pragma: no cover
|
113
|
-
return
|
114
|
-
|
115
101
|
|
116
102
|
models.Manager.list = QueryManager.list
|
117
103
|
models.Manager.df = QueryManager.df
|
118
104
|
models.Manager.search = QueryManager.search
|
119
105
|
models.Manager.lookup = QueryManager.lookup
|
120
|
-
models.Manager.__getitem__ = QueryManager.__getitem__
|
121
106
|
models.Manager._track_run_input_manager = QueryManager._track_run_input_manager
|
122
107
|
# the two lines below would be easy if we could actually inherit; like this,
|
123
108
|
# they're suboptimal
|
lamindb/_query_set.py
CHANGED
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, NamedTuple
|
|
6
6
|
import pandas as pd
|
7
7
|
from django.db import models
|
8
8
|
from django.db.models import F
|
9
|
-
from lamin_utils import logger
|
9
|
+
from lamin_utils import colors, logger
|
10
10
|
from lamindb_setup.core._docs import doc_args
|
11
11
|
from lnschema_core.models import (
|
12
12
|
Artifact,
|
@@ -115,7 +115,7 @@ def get(
|
|
115
115
|
else:
|
116
116
|
assert idlike is None # noqa: S101
|
117
117
|
expressions = process_expressions(registry, expressions)
|
118
|
-
return registry.objects.get(**expressions)
|
118
|
+
return registry.objects.using(qs.db).get(**expressions)
|
119
119
|
|
120
120
|
|
121
121
|
class RecordsList(UserList):
|
@@ -186,6 +186,7 @@ class QuerySet(models.QuerySet):
|
|
186
186
|
if pk_column_name in df.columns:
|
187
187
|
df = df.set_index(pk_column_name)
|
188
188
|
if len(df) == 0:
|
189
|
+
logger.warning(colors.yellow("No records found"))
|
189
190
|
return df
|
190
191
|
if include is not None:
|
191
192
|
if isinstance(include, str):
|
@@ -218,12 +219,15 @@ class QuerySet(models.QuerySet):
|
|
218
219
|
f"{related_ORM.__name__.lower()}__{lookup_str}"
|
219
220
|
)
|
220
221
|
link_df = pd.DataFrame(
|
221
|
-
field.through.objects.values(
|
222
|
+
field.through.objects.using(self.db).values(
|
222
223
|
left_side_link_model, values_expression
|
223
224
|
)
|
224
225
|
)
|
225
226
|
if link_df.shape[0] == 0:
|
226
|
-
|
227
|
+
logger.warning(
|
228
|
+
f"{colors.yellow(expression)} is not shown because no values are found"
|
229
|
+
)
|
230
|
+
continue
|
227
231
|
link_groupby = link_df.groupby(left_side_link_model)[
|
228
232
|
values_expression
|
229
233
|
].apply(list)
|
lamindb/_record.py
CHANGED
@@ -7,7 +7,7 @@ import dj_database_url
|
|
7
7
|
import lamindb_setup as ln_setup
|
8
8
|
from django.db import connections, transaction
|
9
9
|
from django.db.models import IntegerField, Manager, Q, QuerySet, Value
|
10
|
-
from lamin_utils import logger
|
10
|
+
from lamin_utils import colors, logger
|
11
11
|
from lamin_utils._lookup import Lookup
|
12
12
|
from lamindb_setup._connect_instance import (
|
13
13
|
get_owner_name_from_identifier,
|
@@ -17,10 +17,11 @@ from lamindb_setup._connect_instance import (
|
|
17
17
|
from lamindb_setup.core._docs import doc_args
|
18
18
|
from lamindb_setup.core._hub_core import connect_instance_hub
|
19
19
|
from lamindb_setup.core._settings_store import instance_settings_file
|
20
|
-
from lnschema_core.models import IsVersioned, Record, Run, Transform
|
20
|
+
from lnschema_core.models import Artifact, Feature, IsVersioned, Record, Run, Transform
|
21
21
|
|
22
22
|
from lamindb._utils import attach_func_to_class_method
|
23
23
|
from lamindb.core._settings import settings
|
24
|
+
from lamindb.core.exceptions import RecordNameChangeIntegrityError
|
24
25
|
|
25
26
|
if TYPE_CHECKING:
|
26
27
|
import pandas as pd
|
@@ -129,6 +130,7 @@ def __init__(record: Record, *args, **kwargs):
|
|
129
130
|
else:
|
130
131
|
# object is loaded from DB (**kwargs could be omitted below, I believe)
|
131
132
|
super(Record, record).__init__(*args, **kwargs)
|
133
|
+
_store_record_old_name(record)
|
132
134
|
|
133
135
|
|
134
136
|
@classmethod # type:ignore
|
@@ -376,6 +378,8 @@ def using(
|
|
376
378
|
instance: str | None,
|
377
379
|
) -> QuerySet:
|
378
380
|
"""{}""" # noqa: D415
|
381
|
+
from ._query_set import QuerySet
|
382
|
+
|
379
383
|
if instance is None:
|
380
384
|
return QuerySet(model=cls, using=None)
|
381
385
|
owner, name = get_owner_name_from_identifier(instance)
|
@@ -395,15 +399,15 @@ def using(
|
|
395
399
|
if not source_schema.issubset(target_schema):
|
396
400
|
missing_members = source_schema - target_schema
|
397
401
|
logger.warning(
|
398
|
-
f"source schema has additional modules: {missing_members}\nconsider mounting these schema modules to
|
402
|
+
f"source schema has additional modules: {missing_members}\nconsider mounting these schema modules to transfer all metadata"
|
399
403
|
)
|
400
|
-
cache_filepath.write_text(iresult[
|
404
|
+
cache_filepath.write_text(f"{iresult['lnid']}\n{iresult['schema_str']}") # type: ignore
|
401
405
|
settings_file = instance_settings_file(name, owner)
|
402
406
|
db = update_db_using_local(iresult, settings_file)
|
403
407
|
else:
|
404
408
|
isettings = load_instance_settings(settings_file)
|
405
409
|
db = isettings.db
|
406
|
-
cache_filepath.write_text(isettings.uid)
|
410
|
+
cache_filepath.write_text(f"{isettings.uid}\n{','.join(isettings.schema)}") # type: ignore
|
407
411
|
add_db_connection(db, instance)
|
408
412
|
return QuerySet(model=cls, using=instance)
|
409
413
|
|
@@ -470,7 +474,7 @@ def get_transfer_run(record) -> Run:
|
|
470
474
|
cache_filepath = ln_setup.settings.cache_dir / f"instance--{owner}--{name}--uid.txt"
|
471
475
|
if not cache_filepath.exists():
|
472
476
|
raise SystemExit("Need to call .using() before")
|
473
|
-
instance_uid = cache_filepath.read_text()
|
477
|
+
instance_uid = cache_filepath.read_text().split("\n")[0]
|
474
478
|
key = f"transfers/{instance_uid}"
|
475
479
|
uid = instance_uid + "0000"
|
476
480
|
transform = Transform.filter(uid=uid).one_or_none()
|
@@ -582,11 +586,15 @@ def save(self, *args, **kwargs) -> Record:
|
|
582
586
|
with transaction.atomic():
|
583
587
|
revises._revises = None # ensure we don't start a recursion
|
584
588
|
revises.save()
|
589
|
+
check_name_change(self)
|
585
590
|
super(Record, self).save(*args, **kwargs)
|
591
|
+
_store_record_old_name(self)
|
586
592
|
self._revises = None
|
587
593
|
# save unversioned record
|
588
594
|
else:
|
595
|
+
check_name_change(self)
|
589
596
|
super(Record, self).save(*args, **kwargs)
|
597
|
+
_store_record_old_name(self)
|
590
598
|
# perform transfer of many-to-many fields
|
591
599
|
# only supported for Artifact and Collection records
|
592
600
|
if db is not None and db != "default" and using_key is None:
|
@@ -614,6 +622,74 @@ def save(self, *args, **kwargs) -> Record:
|
|
614
622
|
return self
|
615
623
|
|
616
624
|
|
625
|
+
def _store_record_old_name(record: Record):
|
626
|
+
# writes the name to the _name attribute, so we can detect renaming upon save
|
627
|
+
if hasattr(record, "_name_field"):
|
628
|
+
record._name = getattr(record, record._name_field)
|
629
|
+
|
630
|
+
|
631
|
+
def check_name_change(record: Record):
|
632
|
+
"""Warns if a record's name has changed."""
|
633
|
+
if (
|
634
|
+
not record.pk
|
635
|
+
or not hasattr(record, "_name")
|
636
|
+
or not hasattr(record, "_name_field")
|
637
|
+
):
|
638
|
+
return
|
639
|
+
|
640
|
+
old_name = record._name
|
641
|
+
new_name = getattr(record, record._name_field)
|
642
|
+
registry = record.__class__.__name__
|
643
|
+
|
644
|
+
if old_name != new_name:
|
645
|
+
# when a label is renamed, only raise a warning if it has a feature
|
646
|
+
if hasattr(record, "artifacts"):
|
647
|
+
linked_records = (
|
648
|
+
record.artifacts.through.filter(
|
649
|
+
label_ref_is_name=True, **{f"{registry.lower()}_id": record.pk}
|
650
|
+
)
|
651
|
+
.exclude(feature_id=None) # must have a feature
|
652
|
+
.exclude(
|
653
|
+
feature_ref_is_name=None
|
654
|
+
) # must be linked via Curator and therefore part of a featureset
|
655
|
+
.distinct()
|
656
|
+
)
|
657
|
+
artifact_ids = linked_records.list("artifact__uid")
|
658
|
+
n = len(artifact_ids)
|
659
|
+
s = "s" if n > 1 else ""
|
660
|
+
if n > 0:
|
661
|
+
logger.error(
|
662
|
+
f"You are trying to {colors.red('rename label')} from '{old_name}' to '{new_name}'!\n"
|
663
|
+
f" → The following {n} artifact{s} {colors.red('will no longer be validated')}: {artifact_ids}\n\n"
|
664
|
+
f"{colors.bold('To rename this label')}, make it external:\n"
|
665
|
+
f" → run `artifact.labels.make_external(label)`\n\n"
|
666
|
+
f"After renaming, consider re-curating the above artifact{s}:\n"
|
667
|
+
f' → in each dataset, manually modify label "{old_name}" to "{new_name}"\n'
|
668
|
+
f" → run `ln.Curator`\n"
|
669
|
+
)
|
670
|
+
raise RecordNameChangeIntegrityError
|
671
|
+
|
672
|
+
# when a feature is renamed
|
673
|
+
elif isinstance(record, Feature):
|
674
|
+
# only internal features are associated with featuresets
|
675
|
+
linked_artifacts = Artifact.filter(feature_sets__features=record).list(
|
676
|
+
"uid"
|
677
|
+
)
|
678
|
+
n = len(linked_artifacts)
|
679
|
+
s = "s" if n > 1 else ""
|
680
|
+
if n > 0:
|
681
|
+
logger.error(
|
682
|
+
f"You are trying to {colors.red('rename feature')} from '{old_name}' to '{new_name}'!\n"
|
683
|
+
f" → The following {n} artifact{s} {colors.red('will no longer be validated')}: {linked_artifacts}\n\n"
|
684
|
+
f"{colors.bold('To rename this feature')}, make it external:\n"
|
685
|
+
" → run `artifact.features.make_external(feature)`\n\n"
|
686
|
+
f"After renaming, consider re-curating the above artifact{s}:\n"
|
687
|
+
f" → in each dataset, manually modify feature '{old_name}' to '{new_name}'\n"
|
688
|
+
f" → run `ln.Curator`\n"
|
689
|
+
)
|
690
|
+
raise RecordNameChangeIntegrityError
|
691
|
+
|
692
|
+
|
617
693
|
def delete(self) -> None:
|
618
694
|
"""Delete the record."""
|
619
695
|
# note that the logic below does not fire if a record is moved to the trash
|
lamindb/core/__init__.py
CHANGED
lamindb/core/_context.py
CHANGED
@@ -438,7 +438,7 @@ class Context:
|
|
438
438
|
)
|
439
439
|
return (
|
440
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)
|
441
|
+
f'ln.track("{ids.base62_12()}0000")\n\n{update_key_note}'
|
442
442
|
)
|
443
443
|
|
444
444
|
# make a new transform record
|
lamindb/core/_data.py
CHANGED
@@ -26,6 +26,7 @@ from lamindb.core._settings import settings
|
|
26
26
|
from ._context import context
|
27
27
|
from ._django import get_artifact_with_related, get_related_model
|
28
28
|
from ._feature_manager import (
|
29
|
+
add_label_feature_links,
|
29
30
|
get_feature_set_links,
|
30
31
|
get_host_id_field,
|
31
32
|
get_label_links,
|
@@ -67,11 +68,17 @@ def add_transform_to_kwargs(kwargs: dict[str, Any], run: Run):
|
|
67
68
|
|
68
69
|
def save_feature_sets(self: Artifact | Collection) -> None:
|
69
70
|
if hasattr(self, "_feature_sets"):
|
71
|
+
from lamindb.core._feature_manager import get_feature_set_by_slot_
|
72
|
+
|
73
|
+
existing_feature_sets = get_feature_set_by_slot_(self)
|
70
74
|
saved_feature_sets = {}
|
71
75
|
for key, feature_set in self._feature_sets.items():
|
72
76
|
if isinstance(feature_set, FeatureSet) and feature_set._state.adding:
|
73
77
|
feature_set.save()
|
74
78
|
saved_feature_sets[key] = feature_set
|
79
|
+
if key in existing_feature_sets:
|
80
|
+
# remove existing feature set on the same slot
|
81
|
+
self.feature_sets.remove(existing_feature_sets[key])
|
75
82
|
if len(saved_feature_sets) > 0:
|
76
83
|
s = "s" if len(saved_feature_sets) > 1 else ""
|
77
84
|
display_feature_set_keys = ",".join(
|
@@ -305,6 +312,8 @@ def add_labels(
|
|
305
312
|
feature: Feature | None = None,
|
306
313
|
*,
|
307
314
|
field: StrField | None = None,
|
315
|
+
feature_ref_is_name: bool | None = None,
|
316
|
+
label_ref_is_name: bool | None = None,
|
308
317
|
) -> None:
|
309
318
|
"""{}""" # noqa: D415
|
310
319
|
if self._state.adding:
|
@@ -373,14 +382,17 @@ def add_labels(
|
|
373
382
|
if registry_name not in self.features._accessor_by_registry:
|
374
383
|
logger.warning(f"skipping {registry_name}")
|
375
384
|
continue
|
376
|
-
|
377
|
-
|
385
|
+
if len(records) == 0:
|
386
|
+
continue
|
387
|
+
features_labels = {
|
388
|
+
registry_name: [(feature, label_record) for label_record in records]
|
389
|
+
}
|
390
|
+
add_label_feature_links(
|
391
|
+
self.features,
|
392
|
+
features_labels,
|
393
|
+
feature_ref_is_name=feature_ref_is_name,
|
394
|
+
label_ref_is_name=label_ref_is_name,
|
378
395
|
)
|
379
|
-
# remove labels that are already linked as add doesn't perform update
|
380
|
-
linked_labels = [r for r in records if r in labels_accessor.filter()]
|
381
|
-
if len(linked_labels) > 0:
|
382
|
-
labels_accessor.remove(*linked_labels)
|
383
|
-
labels_accessor.add(*records, through_defaults={"feature_id": feature.id})
|
384
396
|
links_feature_set = get_feature_set_links(self)
|
385
397
|
feature_set_ids = [link.featureset_id for link in links_feature_set.all()]
|
386
398
|
# get all linked features of type Feature
|
lamindb/core/_django.py
CHANGED
@@ -6,7 +6,7 @@ from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
|
|
6
6
|
from django.db.models.functions import JSONObject
|
7
7
|
from lnschema_core.models import Artifact, FeatureSet, Record
|
8
8
|
|
9
|
-
from .schema import dict_related_model_to_related_name
|
9
|
+
from .schema import dict_related_model_to_related_name, get_schemas_modules
|
10
10
|
|
11
11
|
|
12
12
|
def get_related_model(model, field_name):
|
@@ -35,22 +35,36 @@ def get_artifact_with_related(
|
|
35
35
|
"""Fetch an artifact with its related data."""
|
36
36
|
from lamindb._can_validate import get_name_field
|
37
37
|
|
38
|
+
from ._label_manager import LABELS_EXCLUDE_SET
|
39
|
+
|
38
40
|
model = artifact.__class__
|
39
|
-
|
41
|
+
schema_modules = get_schemas_modules(artifact._state.db)
|
42
|
+
|
43
|
+
foreign_key_fields = [
|
44
|
+
f.name
|
45
|
+
for f in model._meta.fields
|
46
|
+
if f.is_relation and f.related_model.__get_schema_name__() in schema_modules
|
47
|
+
]
|
40
48
|
|
41
49
|
m2m_relations = (
|
42
50
|
[]
|
43
51
|
if not include_m2m
|
44
52
|
else [
|
45
53
|
v
|
46
|
-
for v in dict_related_model_to_related_name(
|
47
|
-
|
54
|
+
for v in dict_related_model_to_related_name(
|
55
|
+
model, instance=artifact._state.db
|
56
|
+
).values()
|
57
|
+
if not v.startswith("_") and v not in LABELS_EXCLUDE_SET
|
48
58
|
]
|
49
59
|
)
|
50
60
|
link_tables = (
|
51
61
|
[]
|
52
62
|
if not include_feature_link
|
53
|
-
else list(
|
63
|
+
else list(
|
64
|
+
dict_related_model_to_related_name(
|
65
|
+
model, links=True, instance=artifact._state.db
|
66
|
+
).values()
|
67
|
+
)
|
54
68
|
)
|
55
69
|
|
56
70
|
# Clear previous queries
|
lamindb/core/_feature_manager.py
CHANGED
@@ -13,6 +13,7 @@ from django.contrib.postgres.aggregates import ArrayAgg
|
|
13
13
|
from django.db import connections
|
14
14
|
from django.db.models import Aggregate
|
15
15
|
from lamin_utils import colors, logger
|
16
|
+
from lamindb_setup.core.hashing import hash_set
|
16
17
|
from lamindb_setup.core.upath import create_path
|
17
18
|
from lnschema_core.models import (
|
18
19
|
Artifact,
|
@@ -216,9 +217,11 @@ def _print_categoricals(
|
|
216
217
|
if not print_params:
|
217
218
|
labels_msg = ""
|
218
219
|
labels_by_feature = defaultdict(list)
|
219
|
-
for _, (_, links) in get_labels_as_dict(
|
220
|
+
for _, (_, links) in get_labels_as_dict(
|
221
|
+
self, links=True, instance=self._state.db
|
222
|
+
).items():
|
220
223
|
for link in links:
|
221
|
-
if link.feature_id is not None:
|
224
|
+
if hasattr(link, "feature_id") and link.feature_id is not None:
|
222
225
|
link_attr = get_link_attr(link, self)
|
223
226
|
labels_by_feature[link.feature_id].append(
|
224
227
|
getattr(link, link_attr).name
|
@@ -584,6 +587,48 @@ def _accessor_by_registry(self):
|
|
584
587
|
return self._accessor_by_registry_
|
585
588
|
|
586
589
|
|
590
|
+
def add_label_feature_links(
|
591
|
+
self,
|
592
|
+
features_labels,
|
593
|
+
*,
|
594
|
+
label_ref_is_name: bool | None = None,
|
595
|
+
feature_ref_is_name: bool | None = None,
|
596
|
+
):
|
597
|
+
if list(features_labels.keys()) != ["ULabel"]:
|
598
|
+
related_names = dict_related_model_to_related_name(self._host.__class__)
|
599
|
+
else:
|
600
|
+
related_names = {"ULabel": "ulabels"}
|
601
|
+
for class_name, registry_features_labels in features_labels.items():
|
602
|
+
related_name = related_names[class_name] # e.g., "ulabels"
|
603
|
+
LinkORM = getattr(self._host, related_name).through
|
604
|
+
field_name = f"{get_link_attr(LinkORM, self._host)}_id" # e.g., ulabel_id
|
605
|
+
links = [
|
606
|
+
LinkORM(
|
607
|
+
**{
|
608
|
+
"artifact_id": self._host.id,
|
609
|
+
"feature_id": feature.id,
|
610
|
+
field_name: label.id,
|
611
|
+
"feature_ref_is_name": feature_ref_is_name,
|
612
|
+
"label_ref_is_name": label_ref_is_name,
|
613
|
+
}
|
614
|
+
)
|
615
|
+
for (feature, label) in registry_features_labels
|
616
|
+
]
|
617
|
+
# a link might already exist
|
618
|
+
try:
|
619
|
+
save(links, ignore_conflicts=False)
|
620
|
+
except Exception:
|
621
|
+
save(links, ignore_conflicts=True)
|
622
|
+
# now delete links that were previously saved without a feature
|
623
|
+
LinkORM.filter(
|
624
|
+
**{
|
625
|
+
"artifact_id": self._host.id,
|
626
|
+
"feature_id": None,
|
627
|
+
f"{field_name}__in": [l.id for _, l in registry_features_labels],
|
628
|
+
}
|
629
|
+
).all().delete()
|
630
|
+
|
631
|
+
|
587
632
|
def _add_values(
|
588
633
|
self,
|
589
634
|
values: dict[str, str | int | float | bool],
|
@@ -715,49 +760,9 @@ def _add_values(
|
|
715
760
|
f"Here is how to create ulabels for them:\n\n{hint}"
|
716
761
|
)
|
717
762
|
raise ValidationError(msg)
|
718
|
-
# bulk add all links
|
763
|
+
# bulk add all links
|
719
764
|
if features_labels:
|
720
|
-
|
721
|
-
related_names = dict_related_model_to_related_name(self._host.__class__)
|
722
|
-
else:
|
723
|
-
related_names = {"ULabel": "ulabels"}
|
724
|
-
for class_name, registry_features_labels in features_labels.items():
|
725
|
-
related_name = related_names[class_name] # e.g., "ulabels"
|
726
|
-
LinkORM = getattr(self._host, related_name).through
|
727
|
-
field_name = f"{get_link_attr(LinkORM, self._host)}_id" # e.g., ulabel_id
|
728
|
-
links = [
|
729
|
-
LinkORM(
|
730
|
-
**{
|
731
|
-
"artifact_id": self._host.id,
|
732
|
-
"feature_id": feature.id,
|
733
|
-
field_name: label.id,
|
734
|
-
}
|
735
|
-
)
|
736
|
-
for (feature, label) in registry_features_labels
|
737
|
-
]
|
738
|
-
# a link might already exist
|
739
|
-
try:
|
740
|
-
save(links, ignore_conflicts=False)
|
741
|
-
except Exception:
|
742
|
-
save(links, ignore_conflicts=True)
|
743
|
-
# now deal with links that were previously saved without a feature_id
|
744
|
-
links_saved = LinkORM.filter(
|
745
|
-
**{
|
746
|
-
"artifact_id": self._host.id,
|
747
|
-
f"{field_name}__in": [
|
748
|
-
l.id for _, l in registry_features_labels
|
749
|
-
],
|
750
|
-
}
|
751
|
-
)
|
752
|
-
for link in links_saved.all():
|
753
|
-
# TODO: also check for inconsistent features
|
754
|
-
if link.feature_id is None:
|
755
|
-
link.feature_id = [
|
756
|
-
f.id
|
757
|
-
for f, l in registry_features_labels
|
758
|
-
if l.id == getattr(link, field_name)
|
759
|
-
][0]
|
760
|
-
link.save()
|
765
|
+
add_label_feature_links(self, features_labels)
|
761
766
|
if _feature_values:
|
762
767
|
save(_feature_values)
|
763
768
|
if is_param:
|
@@ -1004,6 +1009,36 @@ def _add_from(self, data: Artifact | Collection, transfer_logs: dict = None):
|
|
1004
1009
|
self._host.features.add_feature_set(feature_set_self, slot)
|
1005
1010
|
|
1006
1011
|
|
1012
|
+
def make_external(self, feature: Feature) -> None:
|
1013
|
+
"""Make a feature external, aka, remove feature from feature sets.
|
1014
|
+
|
1015
|
+
Args:
|
1016
|
+
feature: `Feature` A feature record.
|
1017
|
+
|
1018
|
+
"""
|
1019
|
+
if not isinstance(feature, Feature):
|
1020
|
+
raise TypeError("feature must be a Feature record!")
|
1021
|
+
feature_sets = FeatureSet.filter(features=feature).all()
|
1022
|
+
for fs in feature_sets:
|
1023
|
+
f = Feature.filter(uid=feature.uid).all()
|
1024
|
+
features_updated = fs.members.difference(f)
|
1025
|
+
if len(features_updated) > 0:
|
1026
|
+
# re-compute the hash of feature sets based on the updated members
|
1027
|
+
features_hash = hash_set({feature.uid for feature in features_updated})
|
1028
|
+
fs.hash = features_hash
|
1029
|
+
fs.n = len(features_updated)
|
1030
|
+
fs.save()
|
1031
|
+
# delete the link between the feature and the feature set
|
1032
|
+
FeatureSet.features.through.objects.filter(
|
1033
|
+
feature_id=feature.id, featureset_id=fs.id
|
1034
|
+
).delete()
|
1035
|
+
# if no members are left in the featureset, delete it
|
1036
|
+
if len(features_updated) == 0:
|
1037
|
+
logger.warning(f"deleting empty feature set: {fs}")
|
1038
|
+
fs.artifacts.set([])
|
1039
|
+
fs.delete()
|
1040
|
+
|
1041
|
+
|
1007
1042
|
FeatureManager.__init__ = __init__
|
1008
1043
|
ParamManager.__init__ = __init__
|
1009
1044
|
FeatureManager.__repr__ = __repr__
|
@@ -1020,6 +1055,7 @@ FeatureManager._add_set_from_mudata = _add_set_from_mudata
|
|
1020
1055
|
FeatureManager._add_from = _add_from
|
1021
1056
|
FeatureManager.filter = filter
|
1022
1057
|
FeatureManager.get = get
|
1058
|
+
FeatureManager.make_external = make_external
|
1023
1059
|
ParamManager.add_values = add_values_params
|
1024
1060
|
ParamManager.get_values = get_values
|
1025
1061
|
ParamManager.filter = filter
|