lamindb 1.5.0__py3-none-any.whl → 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lamindb/__init__.py CHANGED
@@ -94,7 +94,7 @@ Low-level functionality.
94
94
 
95
95
  # ruff: noqa: I001
96
96
  # denote a release candidate for 0.1.0 with 0.1rc1, 0.1a1, 0.1b1, etc.
97
- __version__ = "1.5.0"
97
+ __version__ = "1.5.1"
98
98
 
99
99
  import warnings
100
100
 
lamindb/core/_context.py CHANGED
@@ -259,8 +259,8 @@ class Context:
259
259
  self,
260
260
  transform: str | Transform | None = None,
261
261
  *,
262
- project: str | None = None,
263
- space: str | None = None,
262
+ project: str | Project | None = None,
263
+ space: str | Space | None = None,
264
264
  params: dict | None = None,
265
265
  new_run: bool | None = None,
266
266
  path: str | None = None,
@@ -273,9 +273,10 @@ class Context:
273
273
 
274
274
  Args:
275
275
  transform: A transform (stem) `uid` (or record). If `None`, auto-creates a `transform` with its `uid`.
276
- project: A project `name` or `uid` for labeling entities created during the run.
277
- space: A space `name` or `uid` to identify where potentially sensitive entities are created during the run.
278
- This doesn't affect `Storage`, `ULabel`, `Feature`, `Schema`, `Param` and bionty entities as these provide mere structure that should typically be commonly accessible.
276
+ project: A project, its `name` or `uid` for labeling entities created during the run.
277
+ space: A restricted space, its `name` or `uid` for creating sensitive entities are created during the run.
278
+ The default is the common `"All"` space that every LaminDB instance has.
279
+ The `space` argument doesn't affect `Storage`, `ULabel`, `Feature`, `Schema`, `Param` and bionty entities as these provide structure that should typically be commonly accessible.
279
280
  If you want to manually move entities to a different space, set the `.space` field (:doc:`docs:access`).
280
281
  params: A dictionary of parameters to track for the run.
281
282
  new_run: If `False`, loads the latest run of transform
@@ -309,20 +310,32 @@ class Context:
309
310
  if project is None:
310
311
  project = os.environ.get("LAMIN_CURRENT_PROJECT")
311
312
  if project is not None:
312
- project_record = Project.filter(
313
- Q(name=project) | Q(uid=project)
314
- ).one_or_none()
315
- if project_record is None:
316
- raise InvalidArgument(
317
- f"Project '{project}' not found, either create it with `ln.Project(name='...').save()` or fix typos."
313
+ if isinstance(project, Project):
314
+ assert project._state.adding is False, ( # noqa: S101
315
+ "Project must be saved before passing it to track()"
318
316
  )
317
+ project_record = project
318
+ else:
319
+ project_record = Project.filter(
320
+ Q(name=project) | Q(uid=project)
321
+ ).one_or_none()
322
+ if project_record is None:
323
+ raise InvalidArgument(
324
+ f"Project '{project}' not found, either create it with `ln.Project(name='...').save()` or fix typos."
325
+ )
319
326
  self._project = project_record
320
327
  if space is not None:
321
- space_record = Space.filter(Q(name=space) | Q(uid=space)).one_or_none()
322
- if space_record is None:
323
- raise InvalidArgument(
324
- f"Space '{space}', please check on the hub UI whether you have the correct `uid` or `name`."
328
+ if isinstance(space, Space):
329
+ assert space._state.adding is False, ( # noqa: S101
330
+ "Space must be saved before passing it to track()"
325
331
  )
332
+ space_record = space
333
+ else:
334
+ space_record = Space.filter(Q(name=space) | Q(uid=space)).one_or_none()
335
+ if space_record is None:
336
+ raise InvalidArgument(
337
+ f"Space '{space}', please check on the hub UI whether you have the correct `uid` or `name`."
338
+ )
326
339
  self._space = space_record
327
340
  self._logging_message_track = ""
328
341
  self._logging_message_imports = ""
@@ -110,7 +110,7 @@ def save_tiledbsoma_experiment(
110
110
  ) -> Artifact:
111
111
  """Write `AnnData` to `tiledbsoma.Experiment`.
112
112
 
113
- Reads `AnnData` objects, writes them to `tiledbsoma.Experiment`, creates & saves an {class}`~lamindb.Artifact`.
113
+ Reads `AnnData` objects, writes them to `tiledbsoma.Experiment`, creates & saves an :class:`~lamindb.Artifact`.
114
114
 
115
115
  Populates a column `lamin_run_uid` column in `obs` with the current `run.uid`.
116
116
 
@@ -202,28 +202,44 @@ def save_tiledbsoma_experiment(
202
202
  context=ctx,
203
203
  )
204
204
 
205
+ prepare_experiment = False
205
206
  resize_experiment = False
206
207
  if registration_mapping is not None:
207
- if version.parse(soma.__version__) < version.parse("1.15.0rc4"):
208
+ soma_version_parsed = version.parse(soma.__version__)
209
+ if soma_version_parsed < version.parse("1.15.0rc4"):
208
210
  n_observations = len(registration_mapping.obs_axis.data)
209
211
  else:
210
212
  n_observations = registration_mapping.get_obs_shape()
211
- resize_experiment = True
213
+ prepare_experiment = soma_version_parsed >= version.parse("1.16.2")
214
+ resize_experiment = not prepare_experiment
212
215
  else: # happens only if not appending and only one adata passed
213
216
  assert len(adata_objects) == 1 # noqa: S101
214
217
  n_observations = adata_objects[0].n_obs
215
218
 
216
219
  logger.important(f"Writing the tiledbsoma store to {storepath_str}")
220
+ experiment_exists: bool | None = None
217
221
  for adata_obj in adata_objects:
218
- if resize_experiment and soma.Experiment.exists(storepath_str, context=ctx):
219
- # can only happen if registration_mapping is not None
220
- soma_io.resize_experiment(
221
- storepath_str,
222
- nobs=n_observations,
223
- nvars=registration_mapping.get_var_shapes(),
224
- context=ctx,
225
- )
226
- resize_experiment = False
222
+ # do not recheck if True
223
+ if not experiment_exists and (resize_experiment or prepare_experiment):
224
+ experiment_exists = soma.Experiment.exists(storepath_str, context=ctx)
225
+ if experiment_exists:
226
+ # both can only happen if registration_mapping is not None
227
+ if resize_experiment:
228
+ soma_io.resize_experiment(
229
+ storepath_str,
230
+ nobs=n_observations,
231
+ nvars=registration_mapping.get_var_shapes(),
232
+ context=ctx,
233
+ )
234
+ resize_experiment = False
235
+ elif prepare_experiment:
236
+ registration_mapping.prepare_experiment(storepath_str, context=ctx)
237
+ prepare_experiment = False
238
+ registration_mapping_write = (
239
+ registration_mapping.subset_for_anndata(adata_obj)
240
+ if hasattr(registration_mapping, "subset_for_anndata")
241
+ else registration_mapping
242
+ )
227
243
  soma_io.from_anndata(
228
244
  storepath_str,
229
245
  adata_obj,
@@ -231,7 +247,7 @@ def save_tiledbsoma_experiment(
231
247
  context=ctx,
232
248
  obs_id_name=obs_id_name,
233
249
  var_id_name=var_id_name,
234
- registration_mapping=registration_mapping,
250
+ registration_mapping=registration_mapping_write,
235
251
  **kwargs,
236
252
  )
237
253
 
lamindb/curators/core.py CHANGED
@@ -510,7 +510,7 @@ class DataFrameCurator(Curator):
510
510
  categoricals=categoricals,
511
511
  index=schema.index,
512
512
  slot=slot,
513
- schema_maximal_set=schema.maximal_set,
513
+ maximal_set=schema.maximal_set,
514
514
  )
515
515
 
516
516
  @property
@@ -836,7 +836,7 @@ class SpatialDataCurator(SlotsCurator):
836
836
  sub_slot = split_result[1]
837
837
  data_object = self._dataset.attrs[split_result[1]]
838
838
  data_object = pd.DataFrame([data_object])
839
- self._slots[slot] = DataFrameCurator(data_object, slot_schema)
839
+ self._slots[slot] = DataFrameCurator(data_object, slot_schema, slot)
840
840
  _assign_var_fields_categoricals_multimodal(
841
841
  modality=table_key,
842
842
  slot_type=sub_slot,
@@ -850,27 +850,20 @@ class SpatialDataCurator(SlotsCurator):
850
850
 
851
851
 
852
852
  class CatVector:
853
- """Categorical vector for `DataFrame`.
854
-
855
- Args:
856
- values_getter: A callable or iterable that returns the values to validate.
857
- field: The field to validate against.
858
- key: The name of the column to validate. Only used for logging.
859
- values_setter: A callable that sets the values.
860
- source: The source to validate against.
861
- """
853
+ """Vector with categorical values."""
862
854
 
863
855
  def __init__(
864
856
  self,
865
- values_getter: Callable | Iterable[str],
866
- field: FieldAttr,
867
- key: str,
868
- values_setter: Callable | None = None,
869
- source: Record | None = None,
857
+ values_getter: Callable
858
+ | Iterable[str], # A callable or iterable that returns the values to validate.
859
+ field: FieldAttr, # The field to validate against.
860
+ key: str, # The name of the vector to validate. Only used for logging.
861
+ values_setter: Callable | None = None, # A callable that sets the values.
862
+ source: Record | None = None, # The ontology source to validate against.
870
863
  feature: Feature | None = None,
871
864
  cat_manager: DataFrameCatManager | None = None,
872
865
  subtype_str: str = "",
873
- maximal_set: bool = False, # Passed during validation. Whether unvalidated categoricals cause validation failure.
866
+ maximal_set: bool = True, # whether unvalidated categoricals cause validation failure.
874
867
  ) -> None:
875
868
  self._values_getter = values_getter
876
869
  self._values_setter = values_setter
@@ -912,18 +905,20 @@ class CatVector:
912
905
  @property
913
906
  def is_validated(self) -> bool:
914
907
  """Whether the vector is validated."""
915
- # ensembl gene IDs pass even if they were not validated
916
- # this is a simple solution to the ensembl gene version problem
917
- if self._field.field.attname == "ensembl_gene_id":
918
- # if none of the ensembl gene ids were validated, we are probably not looking at ensembl gene IDs
919
- if len(self.values) == len(self._non_validated):
920
- return False
921
- # if maximal set, we do not allow additional unvalidated genes
922
- elif len(self._non_validated) != 0 and self._maximal_set:
923
- return False
924
- return True
925
- else:
926
- return len(self._non_validated) == 0
908
+ # if nothing was validated, something likely is fundamentally wrong
909
+ # should probably add a setting `at_least_one_validated`
910
+ result = True
911
+ if len(self.values) > 0 and len(self.values) == len(self._non_validated):
912
+ result = False
913
+ # len(self._non_validated) != 0
914
+ # if maximal_set is True, return False
915
+ # if maximal_set is False, return True
916
+ # len(self._non_validated) == 0
917
+ # return True
918
+ if len(self._non_validated) != 0:
919
+ if self._maximal_set:
920
+ result = False
921
+ return result
927
922
 
928
923
  def _replace_synonyms(self) -> list[str]:
929
924
  """Replace synonyms in the vector with standardized values."""
@@ -1078,11 +1073,6 @@ class CatVector:
1078
1073
  field_name = self._field.field.name
1079
1074
  model_field = f"{registry.__name__}.{field_name}"
1080
1075
 
1081
- def _log_mapping_info():
1082
- logger.indent = ""
1083
- logger.info(f'mapping "{self._key}" on {colors.italic(model_field)}')
1084
- logger.indent = " "
1085
-
1086
1076
  kwargs_current = get_current_filter_kwargs(
1087
1077
  registry, {"organism": self._organism, "source": self._source}
1088
1078
  )
@@ -1121,7 +1111,6 @@ class CatVector:
1121
1111
  non_validated = [i for i in non_validated if i not in values_validated]
1122
1112
  n_non_validated = len(non_validated)
1123
1113
  if n_non_validated == 0:
1124
- logger.indent = ""
1125
1114
  logger.success(
1126
1115
  f'"{self._key}" is validated against {colors.italic(model_field)}'
1127
1116
  )
@@ -1143,14 +1132,12 @@ class CatVector:
1143
1132
  warning_message += f" → fix typos, remove non-existent values, or save terms via: {colors.cyan(non_validated_hint_print)}"
1144
1133
  if self._subtype_query_set is not None:
1145
1134
  warning_message += f"\n → a valid label for subtype '{self._subtype_str}' has to be one of {self._subtype_query_set.list('name')}"
1146
- if logger.indent == "":
1147
- _log_mapping_info()
1135
+ logger.info(f'mapping "{self._key}" on {colors.italic(model_field)}')
1148
1136
  logger.warning(warning_message)
1149
1137
  if self._cat_manager is not None:
1150
1138
  self._cat_manager._validate_category_error_messages = strip_ansi_codes(
1151
1139
  warning_message
1152
1140
  )
1153
- logger.indent = ""
1154
1141
  return non_validated, syn_mapper
1155
1142
 
1156
1143
  def validate(self) -> None:
@@ -1218,7 +1205,7 @@ class DataFrameCatManager:
1218
1205
  sources: dict[str, Record] | None = None,
1219
1206
  index: Feature | None = None,
1220
1207
  slot: str | None = None,
1221
- schema_maximal_set: bool = False,
1208
+ maximal_set: bool = False,
1222
1209
  ) -> None:
1223
1210
  self._non_validated = None
1224
1211
  self._index = index
@@ -1235,7 +1222,7 @@ class DataFrameCatManager:
1235
1222
  self._validate_category_error_messages: str = ""
1236
1223
  self._cat_vectors: dict[str, CatVector] = {}
1237
1224
  self._slot = slot
1238
- self._maximal_set = schema_maximal_set
1225
+ self._maximal_set = maximal_set
1239
1226
 
1240
1227
  if columns_names is None:
1241
1228
  columns_names = []
@@ -1280,7 +1267,6 @@ class DataFrameCatManager:
1280
1267
  feature=feature,
1281
1268
  cat_manager=self,
1282
1269
  subtype_str=subtype_str,
1283
- maximal_set=self._maximal_set,
1284
1270
  )
1285
1271
  if index is not None and index.dtype.startswith("cat"):
1286
1272
  result = parse_dtype(index.dtype)[0]
@@ -1292,7 +1278,6 @@ class DataFrameCatManager:
1292
1278
  key=key,
1293
1279
  feature=index,
1294
1280
  cat_manager=self,
1295
- maximal_set=self._maximal_set,
1296
1281
  )
1297
1282
 
1298
1283
  @property
@@ -1330,7 +1315,7 @@ class DataFrameCatManager:
1330
1315
 
1331
1316
  validated = True
1332
1317
  for key, cat_vector in self._cat_vectors.items():
1333
- logger.info(f"validating column {key}")
1318
+ logger.info(f"validating vector {key}")
1334
1319
  cat_vector.validate()
1335
1320
  validated &= cat_vector.is_validated
1336
1321
  self._is_validated = validated
@@ -1493,6 +1478,9 @@ def annotate_artifact(
1493
1478
  else "columns"
1494
1479
  )
1495
1480
  features = slot_curator.cat._cat_vectors[name].records
1481
+ if features is None:
1482
+ logger.warning(f"no features found for slot {slot}")
1483
+ continue
1496
1484
  itype = parse_cat_dtype(artifact.schema.slots[slot].itype, is_itype=True)[
1497
1485
  "field"
1498
1486
  ]
@@ -0,0 +1,16 @@
1
+ # Generated by Django 5.2 on 2025-05-07 12:16
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("lamindb", "0092_alter_artifactfeaturevalue_artifact_and_more"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AlterUniqueTogether(
13
+ name="schemacomponent",
14
+ unique_together={("composite", "slot"), ("composite", "slot", "component")},
15
+ ),
16
+ ]
@@ -49,7 +49,7 @@ from ._label_manager import _get_labels, describe_labels
49
49
  from ._relations import (
50
50
  dict_related_model_to_related_name,
51
51
  )
52
- from .feature import Feature, FeatureValue
52
+ from .feature import Feature, FeatureValue, parse_dtype
53
53
  from .record import Record
54
54
  from .run import Param, ParamManager, ParamManagerRun, ParamValue, Run
55
55
  from .ulabel import ULabel
@@ -649,13 +649,22 @@ def filter_base(cls, _skip_validation: bool = True, **expression) -> QuerySet:
649
649
  if cls == FeatureManager:
650
650
  from .artifact import ArtifactFeatureValue
651
651
 
652
- return Artifact.objects.exclude(
653
- id__in=Subquery(
654
- ArtifactFeatureValue.objects.filter(
655
- featurevalue__feature=feature
656
- ).values("artifact_id")
652
+ if value: # True
653
+ return Artifact.objects.exclude(
654
+ id__in=Subquery(
655
+ ArtifactFeatureValue.objects.filter(
656
+ featurevalue__feature=feature
657
+ ).values("artifact_id")
658
+ )
659
+ )
660
+ else:
661
+ return Artifact.objects.exclude(
662
+ id__in=Subquery(
663
+ ArtifactFeatureValue.objects.filter(
664
+ featurevalue__feature=feature
665
+ ).values("artifact_id")
666
+ )
657
667
  )
658
- )
659
668
  if comparator in {"__startswith", "__contains"}:
660
669
  logger.important(
661
670
  f"currently not supporting `{comparator}`, using `__icontains` instead"
@@ -667,7 +676,14 @@ def filter_base(cls, _skip_validation: bool = True, **expression) -> QuerySet:
667
676
  elif isinstance(value, (str, Record, bool)):
668
677
  if comparator == "__isnull":
669
678
  if cls == FeatureManager:
670
- return Artifact.objects.exclude(links_ulabel__feature=feature)
679
+ result = parse_dtype(feature.dtype)[0]
680
+ kwargs = {
681
+ f"links_{result['registry'].__name__.lower()}__feature": feature
682
+ }
683
+ if value: # True
684
+ return Artifact.objects.exclude(**kwargs)
685
+ else:
686
+ return Artifact.objects.filter(**kwargs)
671
687
  else:
672
688
  # because SQL is sensitive to whether querying with __in or not
673
689
  # and might return multiple equivalent records for the latter
@@ -17,7 +17,6 @@ from django.db.models import CASCADE, PROTECT, Q
17
17
  from lamin_utils import colors, logger
18
18
  from lamindb_setup import settings as setup_settings
19
19
  from lamindb_setup._init_instance import register_storage_in_instance
20
- from lamindb_setup.core import doc_args
21
20
  from lamindb_setup.core._settings_storage import init_storage
22
21
  from lamindb_setup.core.hashing import HASH_LENGTH, hash_dir, hash_file
23
22
  from lamindb_setup.core.types import UPathStr
@@ -99,8 +98,6 @@ WARNING_RUN_TRANSFORM = "no run & transform got linked, call `ln.track()` & re-r
99
98
 
100
99
  WARNING_NO_INPUT = "run input wasn't tracked, call `ln.track()` and re-run"
101
100
 
102
- DEBUG_KWARGS_DOC = "**kwargs: Internal arguments for debugging."
103
-
104
101
  try:
105
102
  from ..core.storage._zarr import identify_zarr_type
106
103
  except ImportError:
@@ -914,7 +911,7 @@ def add_labels(
914
911
  for registry_name, records in records_by_registry.items():
915
912
  if not from_curator and feature.name in internal_features:
916
913
  raise ValidationError(
917
- "Cannot manually annotate internal feature with label. Please use ln.Curator"
914
+ "Cannot manually annotate a feature measured *within* the dataset. Please use a Curator."
918
915
  )
919
916
  if registry_name not in feature.dtype:
920
917
  if not feature.dtype.startswith("cat"):
@@ -1236,7 +1233,7 @@ class Artifact(Record, IsVersioned, TracksRun, TracksUpdates):
1236
1233
  default=None,
1237
1234
  related_name="validated_artifacts",
1238
1235
  )
1239
- """The schema that validated this artifact in a :class:`~lamindb.curators.Curator`."""
1236
+ """The schema that validated this artifact in a :class:`~lamindb.curators.core.Curator`."""
1240
1237
  feature_sets: Schema = models.ManyToManyField(
1241
1238
  Schema, related_name="artifacts", through="ArtifactSchema"
1242
1239
  )
@@ -2397,7 +2394,6 @@ class Artifact(Record, IsVersioned, TracksRun, TracksUpdates):
2397
2394
  _track_run_input(self, is_run_input)
2398
2395
  return access_memory
2399
2396
 
2400
- @doc_args(DEBUG_KWARGS_DOC)
2401
2397
  def cache(
2402
2398
  self, *, is_run_input: bool | None = None, mute: bool = False, **kwargs
2403
2399
  ) -> Path:
@@ -2410,7 +2406,6 @@ class Artifact(Record, IsVersioned, TracksRun, TracksUpdates):
2410
2406
  Args:
2411
2407
  mute: Silence logging of caching progress.
2412
2408
  is_run_input: Whether to track this artifact as run input.
2413
- {}
2414
2409
 
2415
2410
  Example::
2416
2411
 
@@ -2560,13 +2555,11 @@ class Artifact(Record, IsVersioned, TracksRun, TracksUpdates):
2560
2555
  if delete_msg != "did-not-delete":
2561
2556
  logger.success(f"deleted {colors.yellow(f'{path}')}")
2562
2557
 
2563
- @doc_args(DEBUG_KWARGS_DOC)
2564
2558
  def save(self, upload: bool | None = None, **kwargs) -> Artifact:
2565
2559
  """Save to database & storage.
2566
2560
 
2567
2561
  Args:
2568
2562
  upload: Trigger upload to cloud storage in instances with hybrid storage mode.
2569
- {}
2570
2563
 
2571
2564
  Example::
2572
2565
 
@@ -2763,8 +2756,8 @@ def _track_run_input(
2763
2756
  # record is on another db
2764
2757
  # we have to save the record into the current db with
2765
2758
  # the run being attached to a transfer transform
2766
- logger.important(
2767
- f"completing transfer to track {data.__class__.__name__}('{data.uid[:8]}') as input"
2759
+ logger.info(
2760
+ f"completing transfer to track {data.__class__.__name__}('{data.uid[:8]}...') as input"
2768
2761
  )
2769
2762
  data.save()
2770
2763
  is_valid = True
@@ -84,10 +84,44 @@ class HasParents:
84
84
  return view_parents(
85
85
  record=self, # type: ignore
86
86
  field=field,
87
+ with_parents=True,
87
88
  with_children=with_children,
88
89
  distance=distance,
89
90
  )
90
91
 
92
+ def view_children(
93
+ self,
94
+ field: StrField | None = None,
95
+ distance: int = 5,
96
+ ):
97
+ """View children in an ontology.
98
+
99
+ Args:
100
+ field: Field to display on graph
101
+ distance: Maximum distance still shown.
102
+
103
+ Ontological hierarchies: :class:`~lamindb.ULabel` (project & sub-project), :class:`~bionty.CellType` (cell type & subtype).
104
+
105
+ Examples:
106
+ >>> import bionty as bt
107
+ >>> bt.Tissue.from_source(name="subsegmental bronchus").save()
108
+ >>> record = bt.Tissue.get(name="respiratory tube")
109
+ >>> record.view_parents()
110
+ >>> tissue.view_parents(with_children=True)
111
+ """
112
+ if field is None:
113
+ field = get_name_field(self)
114
+ if not isinstance(field, str):
115
+ field = field.field.name
116
+
117
+ return view_parents(
118
+ record=self, # type: ignore
119
+ field=field,
120
+ with_parents=False,
121
+ with_children=True,
122
+ distance=distance,
123
+ )
124
+
91
125
  def query_parents(self) -> QuerySet:
92
126
  """Query parents in an ontology."""
93
127
  return _query_relatives([self], "parents", self.__class__) # type: ignore
@@ -210,6 +244,7 @@ def view_lineage(
210
244
  def view_parents(
211
245
  record: Record,
212
246
  field: str,
247
+ with_parents: bool = True,
213
248
  with_children: bool = False,
214
249
  distance: int = 100,
215
250
  attr_name: Literal["parents", "predecessors"] = "parents",
@@ -223,11 +258,12 @@ def view_parents(
223
258
  import pandas as pd
224
259
 
225
260
  df_edges = None
226
- df_edges_parents = _df_edges_from_parents(
227
- record=record, field=field, distance=distance, attr_name=attr_name
228
- )
229
- if df_edges_parents is not None:
230
- df_edges = df_edges_parents
261
+ df_edges_parents = None
262
+ df_edges_children = None
263
+ if with_parents:
264
+ df_edges_parents = _df_edges_from_parents(
265
+ record=record, field=field, distance=distance, attr_name=attr_name
266
+ )
231
267
  if with_children:
232
268
  df_edges_children = _df_edges_from_parents(
233
269
  record=record,
@@ -236,13 +272,32 @@ def view_parents(
236
272
  children=True,
237
273
  attr_name=attr_name,
238
274
  )
239
- if df_edges_children is not None:
240
- if df_edges is not None:
241
- df_edges = pd.concat(
242
- [df_edges_parents, df_edges_children]
243
- ).drop_duplicates()
244
- else:
245
- df_edges = df_edges_children
275
+ # Rename the columns to swap source and target
276
+ df_edges_children = df_edges_children.rename(
277
+ columns={
278
+ "source": "temp_target",
279
+ "source_label": "temp_target_label",
280
+ "source_record": "temp_target_record",
281
+ "target": "source",
282
+ "target_label": "source_label",
283
+ "target_record": "source_record",
284
+ }
285
+ )
286
+ df_edges_children = df_edges_children.rename(
287
+ columns={
288
+ "temp_target": "target",
289
+ "temp_target_label": "target_label",
290
+ "temp_target_record": "target_record",
291
+ }
292
+ )
293
+ if df_edges_parents is not None and df_edges_children is not None:
294
+ df_edges = pd.concat([df_edges_parents, df_edges_children]).drop_duplicates()
295
+ elif df_edges_parents is not None:
296
+ df_edges = df_edges_parents
297
+ elif df_edges_children is not None:
298
+ df_edges = df_edges_children
299
+ else:
300
+ return None
246
301
 
247
302
  record_label = _record_label(record, field)
248
303
 
@@ -430,8 +430,9 @@ def reshape_annotate_result(
430
430
  """
431
431
  cols_from_include = cols_from_include or {}
432
432
 
433
- # initialize result with basic fields
434
- result = df[field_names]
433
+ # initialize result with basic fields, need a copy as we're modifying it
434
+ # will give us warnings otherwise
435
+ result = df[field_names].copy()
435
436
  # process features if requested
436
437
  if feature_names:
437
438
  # handle feature_values
lamindb/models/record.py CHANGED
@@ -596,9 +596,8 @@ class Registry(ModelBase):
596
596
 
597
597
  target_modules = setup_settings.instance.modules
598
598
  if missing_members := source_modules - target_modules:
599
- logger.warning(
600
- f"source modules has additional modules: {missing_members}\n"
601
- "consider mounting these registry modules to transfer all metadata"
599
+ logger.info(
600
+ f"in transfer, source lamindb instance has additional modules: {', '.join(missing_members)}"
602
601
  )
603
602
 
604
603
  add_db_connection(db, instance)
@@ -839,7 +838,7 @@ class BasicRecord(models.Model, metaclass=Registry):
839
838
  self.features._add_from(self_on_db, transfer_logs=transfer_logs)
840
839
  self.labels.add_from(self_on_db, transfer_logs=transfer_logs)
841
840
  for k, v in transfer_logs.items():
842
- if k != "run":
841
+ if k != "run" and len(v) > 0:
843
842
  logger.important(f"{k} records: {', '.join(v)}")
844
843
 
845
844
  if (
lamindb/models/schema.py CHANGED
@@ -439,7 +439,7 @@ class Schema(Record, CanCurate, TracksRun):
439
439
  artifacts: Artifact
440
440
  """The artifacts that measure a feature set that matches this schema."""
441
441
  validated_artifacts: Artifact
442
- """The artifacts that were validated against this schema with a :class:`~lamindb.curators.Curator`."""
442
+ """The artifacts that were validated against this schema with a :class:`~lamindb.curators.core.Curator`."""
443
443
  projects: Project
444
444
  """Linked projects."""
445
445
  _curation: dict[str, Any] = JSONField(default=None, db_default=None, null=True)
@@ -457,7 +457,7 @@ class Schema(Record, CanCurate, TracksRun):
457
457
  # For instance, the set of measured features might be a superset of the minimally required set of features.
458
458
  # """
459
459
  # validated_schemas: Schema
460
- # """The schemas that were validated against this schema with a :class:`~lamindb.curators.Curator`."""
460
+ # """The schemas that were validated against this schema with a :class:`~lamindb.curators.core.Curator`."""
461
461
  composite: Schema | None = ForeignKey(
462
462
  "self", PROTECT, related_name="+", default=None, null=True
463
463
  )
@@ -538,7 +538,6 @@ class Schema(Record, CanCurate, TracksRun):
538
538
  optional_features,
539
539
  features_registry,
540
540
  flexible,
541
- list_for_hashing,
542
541
  ) = self._validate_kwargs_calculate_hash(
543
542
  features=features,
544
543
  index=index,
@@ -562,7 +561,6 @@ class Schema(Record, CanCurate, TracksRun):
562
561
  .filter(hash=validated_kwargs["hash"])
563
562
  .one_or_none()
564
563
  )
565
- self._list_for_hashing = list_for_hashing
566
564
  if schema is not None:
567
565
  logger.important(f"returning existing schema with same hash: {schema}")
568
566
  init_self_from_db(self, schema)
@@ -609,7 +607,7 @@ class Schema(Record, CanCurate, TracksRun):
609
607
  coerce_dtype: bool,
610
608
  n_features: int | None,
611
609
  optional_features_manual: list[Feature] | None = None,
612
- ) -> tuple[list[Feature], dict[str, Any], list[Feature], Registry, bool, list[str]]:
610
+ ) -> tuple[list[Feature], dict[str, Any], list[Feature], Registry, bool]:
613
611
  optional_features = []
614
612
  features_registry: Registry = None
615
613
  if itype is not None:
@@ -729,7 +727,6 @@ class Schema(Record, CanCurate, TracksRun):
729
727
  optional_features,
730
728
  features_registry,
731
729
  flexible,
732
- list_for_hashing,
733
730
  )
734
731
 
735
732
  @classmethod
@@ -865,26 +862,24 @@ class Schema(Record, CanCurate, TracksRun):
865
862
  if hasattr(self, "_features")
866
863
  else (self.members.list() if self.members.exists() else [])
867
864
  )
868
- _, validated_kwargs, _, _, _, list_for_hashing = (
869
- self._validate_kwargs_calculate_hash(
870
- features=features, # type: ignore
871
- index=None, # need to pass None here as otherwise counting double
872
- slots=self._slots if hasattr(self, "_slots") else self.slots,
873
- name=self.name,
874
- description=self.description,
875
- itype=self.itype,
876
- flexible=self.flexible,
877
- type=self.type,
878
- is_type=self.is_type,
879
- otype=self.otype,
880
- dtype=self.dtype,
881
- minimal_set=self.minimal_set,
882
- ordered_set=self.ordered_set,
883
- maximal_set=self.maximal_set,
884
- coerce_dtype=self.coerce_dtype,
885
- n_features=self.n,
886
- optional_features_manual=self.optionals.get(),
887
- )
865
+ _, validated_kwargs, _, _, _ = self._validate_kwargs_calculate_hash(
866
+ features=features, # type: ignore
867
+ index=None, # need to pass None here as otherwise counting double
868
+ slots=self.slots,
869
+ name=self.name,
870
+ description=self.description,
871
+ itype=self.itype,
872
+ flexible=self.flexible,
873
+ type=self.type,
874
+ is_type=self.is_type,
875
+ otype=self.otype,
876
+ dtype=self.dtype,
877
+ minimal_set=self.minimal_set,
878
+ ordered_set=self.ordered_set,
879
+ maximal_set=self.maximal_set,
880
+ coerce_dtype=self.coerce_dtype,
881
+ n_features=self.n,
882
+ optional_features_manual=self.optionals.get(),
888
883
  )
889
884
  if validated_kwargs["hash"] != self.hash:
890
885
  from .artifact import Artifact
@@ -896,7 +891,6 @@ class Schema(Record, CanCurate, TracksRun):
896
891
  )
897
892
  self.hash = validated_kwargs["hash"]
898
893
  self.n = validated_kwargs["n"]
899
- self._list_for_hashing = list_for_hashing
900
894
  super().save(*args, **kwargs)
901
895
  if hasattr(self, "_slots"):
902
896
  # analogous to save_schema_links in core._data.py
@@ -910,6 +904,7 @@ class Schema(Record, CanCurate, TracksRun):
910
904
  }
911
905
  links.append(Schema.components.through(**kwargs))
912
906
  bulk_create(links, ignore_conflicts=True)
907
+ delattr(self, "_slots")
913
908
  if hasattr(self, "_features"):
914
909
  assert self.n > 0 # noqa: S101
915
910
  using: bool | None = kwargs.pop("using", None)
@@ -1188,7 +1183,7 @@ class SchemaComponent(BasicRecord, LinkORM, TracksRun):
1188
1183
  slot: str | None = CharField(null=True)
1189
1184
 
1190
1185
  class Meta:
1191
- unique_together = (("composite", "component"), ("composite", "slot"))
1186
+ unique_together = (("composite", "slot", "component"), ("composite", "slot"))
1192
1187
 
1193
1188
 
1194
1189
  Schema._get_related_name = _get_related_name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: lamindb
3
- Version: 1.5.0
3
+ Version: 1.5.1
4
4
  Summary: A data framework for biology.
5
5
  Author-email: Lamin Labs <open-source@lamin.ai>
6
6
  Requires-Python: >=3.10,<3.14
@@ -1,4 +1,4 @@
1
- lamindb/__init__.py,sha256=PvttGVLvK5zlO9ZI651ZzH36fKKUpaj2fAyOCKRnAoE,2676
1
+ lamindb/__init__.py,sha256=LgGBliPC6cKo95tRnosq_SuMBgRGjlQF0wNmEBHuo10,2676
2
2
  lamindb/_finish.py,sha256=Wqb846pCErsx5ZPulAfdF5PJbWzgAdfbuYuf4FndfhY,20124
3
3
  lamindb/_tracked.py,sha256=fse_H0ehc9WvU_l1572g7qya0sRdWCh22LZkq0XU4ic,4445
4
4
  lamindb/_view.py,sha256=kSmG8X4ULQZEKxY7ESnthQqsUf1DEzoYGeTLYRU1I7s,4938
@@ -11,7 +11,7 @@ lamindb/base/uids.py,sha256=cLBi5mIlsf1ltkTb17r1FLzlOjlGmjvsCygoVJHQ-A8,2116
11
11
  lamindb/base/users.py,sha256=8MSmAvCKoUF15YsDE6BGLBXsFWpfoEEg8iDTKZ7kD48,848
12
12
  lamindb/core/__init__.py,sha256=aaBq0UVjNolMynbT1V5hB6UrJm1tK0M6WHu_r6em9_4,604
13
13
  lamindb/core/_compat.py,sha256=NLnKk1qk4xdgMV-QwFDnBnbio02ujjlF86icvhpdv4c,2029
14
- lamindb/core/_context.py,sha256=JOvz3YbzZy3zGq_0giLHFZIGl04dMq-0hneUdJMTZes,33989
14
+ lamindb/core/_context.py,sha256=xKj4YGpgM3Dx8H7_rNf6EI3sC5JHScSYVgHw-QYbMp4,34679
15
15
  lamindb/core/_mapped_collection.py,sha256=dxyZ1ZHFn5SBl1xILqN9N6TTUJP0PptVBV-2O0EdZww,25751
16
16
  lamindb/core/_settings.py,sha256=DAeEN2Qswj6VDlM7OE5YtoteMfFZ61CmMwcS056_scE,6211
17
17
  lamindb/core/_sync_git.py,sha256=Z7keuyS5X7CAj285sEbZIFExZF9mtjGH8DzKwz3xhHw,5881
@@ -30,7 +30,7 @@ lamindb/core/storage/_anndata_sizes.py,sha256=aXO3OB--tF5MChenSsigW6Q-RuE8YJJOUT
30
30
  lamindb/core/storage/_backed_access.py,sha256=LlpRDZ0skseZA5tBFu3-cH1wJwuXm7-NS2RgnTK7wgc,7382
31
31
  lamindb/core/storage/_polars_lazy_df.py,sha256=Z0KMp0OU5S36L5g8EuJk7V_nn-spgG1lFeEFnkTOLcw,1350
32
32
  lamindb/core/storage/_pyarrow_dataset.py,sha256=lRYYt7edUtwauhxd7RwFud6YPDbz2PFvYYgqLhfapfk,1398
33
- lamindb/core/storage/_tiledbsoma.py,sha256=gOcfgMHToI142KqyOYWJMOzmFMLos660k6ZFaAooYPc,10308
33
+ lamindb/core/storage/_tiledbsoma.py,sha256=QLMOPjdxv9JFs9JR0Kqg1UTkJKNgwIDMeHAewB0-Lqg,11124
34
34
  lamindb/core/storage/_valid_suffixes.py,sha256=vUSeQ4s01rdhD_vSd6wKmFBsgMJAKkBMnL_T9Y1znMg,501
35
35
  lamindb/core/storage/_zarr.py,sha256=cisYXU4_QXMF_ZY2pV52Incus6365mMxRphLaHO76W0,6801
36
36
  lamindb/core/storage/objects.py,sha256=n1Kj1soxF-_iLFyNnHriVFcngw6nqEAd7aVm0Hm8Tcw,3017
@@ -40,7 +40,7 @@ lamindb/core/subsettings/_annotation_settings.py,sha256=o-yTYw-NmjFmtehbKU8qnf7t
40
40
  lamindb/core/subsettings/_creation_settings.py,sha256=NGHWKqCFSzVNBxAr2VnmdYguiFdW29XUK7T9wRsVshg,906
41
41
  lamindb/curators/__init__.py,sha256=ZexikeaVunT24TqsR1NsSOCSBXDBigfGtFT55tBwqS8,371
42
42
  lamindb/curators/_legacy.py,sha256=dTim3YFvdYyMsn6y8qSYkbCnnEI4tlaevN2-OO_qEx8,76174
43
- lamindb/curators/core.py,sha256=Kd9XAow7DR0BxrjFZ3zH469_11FItoD7RyqkW2zg4vA,60184
43
+ lamindb/curators/core.py,sha256=hbmVGXRwBNxKRRwpS9h9JR7AiVWZzqgI848FBdekDAQ,59818
44
44
  lamindb/curators/_cellxgene_schemas/__init__.py,sha256=zqlFzMNMDGEBe6DV0gBsBMpfc9UHvNv1EpBsz_ktMoA,7502
45
45
  lamindb/curators/_cellxgene_schemas/schema_versions.csv,sha256=X9rmO88TW1Fht1f5mJs0JdW-VPvyKSajpf8lHNeECj4,1680
46
46
  lamindb/examples/__init__.py,sha256=DGImiuWYDvwxh78p5FCwQWClEwsE3ODLU49i_NqbW0c,533
@@ -74,35 +74,36 @@ lamindb/migrations/0090_runproject_project_runs.py,sha256=Ab9wyGxc6xjBfj-36cqdTl
74
74
  lamindb/migrations/0090_squashed.py,sha256=kx_A_25BYantikxCbGhJughFpv_lqyHH86pMh5YevEE,160823
75
75
  lamindb/migrations/0091_alter_featurevalue_options_alter_space_options_and_more.py,sha256=Df4EYAQlLKZ4BpFcsRRF52pGN3hDSo94laiO-V90Kn4,607
76
76
  lamindb/migrations/0092_alter_artifactfeaturevalue_artifact_and_more.py,sha256=x-2Pvi0GJugkLrR--Fw9PBzV-HxqXjl0NktxRtRFJno,2459
77
+ lamindb/migrations/0093_alter_schemacomponent_unique_together.py,sha256=p6pCGU3xzOo5FuHE_COxVn6qLgRUAdfWXJjl4_euEKU,424
77
78
  lamindb/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
79
  lamindb/models/__init__.py,sha256=IFYoZfly3m0Me5Fr8sk6-KdYpVzkuug0lo8jvci00Kg,2080
79
80
  lamindb/models/_describe.py,sha256=4PxaavWidEL8cyV2idbpC_7Zo0Jmjam3X1Bwe71IMaY,5489
80
81
  lamindb/models/_django.py,sha256=2LFaTvIPtxIV8_T6Kx0cvquTetj7C3OcnKukUyC9msY,7705
81
- lamindb/models/_feature_manager.py,sha256=FCZ9Tv0sJv1NPnTanK5O1duIN0MsPVmcIive53nEc1M,53899
82
+ lamindb/models/_feature_manager.py,sha256=uMIcQMYrH1bHCqzovDbkDUOsBre_-mrg_7OS4RdoR8c,54666
82
83
  lamindb/models/_from_values.py,sha256=-8l3_d2Nm14kzi1FjEYvBwyuucL-ZcDSjlMufIb4XoQ,13324
83
84
  lamindb/models/_is_versioned.py,sha256=Th2_cBf9UWh27E6ANxg6LGmjBOumXFy7AjH0GG4FoXA,7601
84
85
  lamindb/models/_label_manager.py,sha256=QOT6mz_rzPJ5p7hM1l-XzDWzyWUERpmAan2n_ma5wpI,12112
85
86
  lamindb/models/_relations.py,sha256=ONjHPiWIa_Ur7zMNTa_9Uw7K-366GORyPvGoVjf4EQs,3681
86
- lamindb/models/artifact.py,sha256=MHEKV-8xuzc5n5-aSyhpGdja2S-ALr0FhsBwhuhhpJw,110368
87
+ lamindb/models/artifact.py,sha256=JDV9I5Pf47UgsyGGd4xgEvzH6sEmG37De7vJouaR4B0,110183
87
88
  lamindb/models/artifact_set.py,sha256=VOZEGDo3m_9Yg_ftx3I2fwdydjHN61X_qV18N6xG4kM,4117
88
89
  lamindb/models/can_curate.py,sha256=5dXHCRoJzLg2y9YDhpH7CyWexxliFHilwJ_UPjjZwRI,29188
89
90
  lamindb/models/collection.py,sha256=TNXnrR86ZgsSfEvaOuAEItgZ947klTXXZspa7hpyVmw,27288
90
91
  lamindb/models/core.py,sha256=A-W_Hdg4AmbBFBU38SEEVhOwSIzww5oNgYAQFnwOO7A,4018
91
92
  lamindb/models/feature.py,sha256=WoT29eZ8DR6MTZgnztbRye3-zX4BRYfJ8HlhdenX2qA,28186
92
93
  lamindb/models/flextable.py,sha256=ET9j0fTFYQIdXOZfwCnosXOag7nYD1DUV6_wZNqhvOs,5400
93
- lamindb/models/has_parents.py,sha256=U-UDu4C3C_lwZo7XA0UbH4bg2kia2Lu16YTPb28cEpw,18456
94
+ lamindb/models/has_parents.py,sha256=A8OWsNotWlFrZB2pURRxp8EcHJ1kIlyV5eMnajGgkh4,20328
94
95
  lamindb/models/project.py,sha256=Hm-5hLn-FffFK3J_68gt-AxVc6bo26fegwGFRw0Gp50,15225
95
96
  lamindb/models/query_manager.py,sha256=mqsULCmUQf5ibpSXazca9ZYxyZwiDLuzSm8s6dPrl_M,10712
96
- lamindb/models/query_set.py,sha256=T8aeXV8W-Wdma_t5eTBeDj54NIlYGimvfEGEh7o2INo,30305
97
- lamindb/models/record.py,sha256=XM5TEnwNgzUJGZYC1n9w499-QVGeYLQ9eYUwVY6msQw,61395
97
+ lamindb/models/query_set.py,sha256=xKh5QjAlHunktB1S4x9f42Fg0SP_-sK7XlyxStIRDSo,30385
98
+ lamindb/models/record.py,sha256=fomXuOcqkfiYF3zEdiUkYw9x00qP5flM7oseUUomhIo,61354
98
99
  lamindb/models/run.py,sha256=FzqVQhYj4DXqlnmHvNIziOCAlx9K0wISXBLpom1Yb74,20688
99
100
  lamindb/models/save.py,sha256=JTAaorKECx0ZeHaX0H9Yt4MDwOsT9F813WbSJkBIPaU,13339
100
- lamindb/models/schema.py,sha256=9RUuHTiFGJAGsxMyyzJ79HrAEDfJ8OmjXGXD7G3yXm4,48040
101
+ lamindb/models/schema.py,sha256=5_31iIPh19eJn0rm1OTeFk0gtD0YUPrTbiPsglKWOeo,47753
101
102
  lamindb/models/transform.py,sha256=LGnTR7g_rAx3YFAFv4l4_UzabruKQlnui1Y3tlWHwXk,12731
102
103
  lamindb/models/ulabel.py,sha256=yn9ttz28MqDBh6ZgwH7cty6GHCJOzLJn2IEpspYosDo,8793
103
104
  lamindb/setup/__init__.py,sha256=OwZpZzPDv5lPPGXZP7-zK6UdO4FHvvuBh439yZvIp3A,410
104
105
  lamindb/setup/core/__init__.py,sha256=SevlVrc2AZWL3uALbE5sopxBnIZPWZ1IB0NBDudiAL8,167
105
- lamindb-1.5.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
106
- lamindb-1.5.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
107
- lamindb-1.5.0.dist-info/METADATA,sha256=g1M_VmflzJYLeuY2Ac_fGLjKZeJtQk19MrK65fOc3EY,2782
108
- lamindb-1.5.0.dist-info/RECORD,,
106
+ lamindb-1.5.1.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
107
+ lamindb-1.5.1.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
108
+ lamindb-1.5.1.dist-info/METADATA,sha256=juLd0ioSI6_wrvJOzGLsfY_8L019WlgLqEnIDj2OolA,2782
109
+ lamindb-1.5.1.dist-info/RECORD,,