lamindb 1.10.2__py3-none-any.whl → 1.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. lamindb/__init__.py +89 -49
  2. lamindb/_finish.py +17 -15
  3. lamindb/_tracked.py +2 -4
  4. lamindb/_view.py +1 -1
  5. lamindb/base/__init__.py +2 -1
  6. lamindb/base/dtypes.py +76 -0
  7. lamindb/core/_settings.py +2 -2
  8. lamindb/core/storage/_anndata_accessor.py +29 -9
  9. lamindb/curators/_legacy.py +16 -3
  10. lamindb/curators/core.py +442 -188
  11. lamindb/errors.py +6 -0
  12. lamindb/examples/cellxgene/__init__.py +8 -3
  13. lamindb/examples/cellxgene/_cellxgene.py +127 -13
  14. lamindb/examples/cellxgene/{cxg_schema_versions.csv → cellxgene_schema_versions.csv} +11 -0
  15. lamindb/examples/croissant/__init__.py +32 -6
  16. lamindb/examples/datasets/__init__.py +2 -2
  17. lamindb/examples/datasets/_core.py +9 -2
  18. lamindb/examples/datasets/_small.py +66 -22
  19. lamindb/examples/fixtures/sheets.py +8 -2
  20. lamindb/integrations/_croissant.py +34 -11
  21. lamindb/migrations/0119_squashed.py +5 -2
  22. lamindb/migrations/0120_add_record_fk_constraint.py +64 -0
  23. lamindb/migrations/0121_recorduser.py +60 -0
  24. lamindb/models/__init__.py +4 -1
  25. lamindb/models/_describe.py +2 -2
  26. lamindb/models/_feature_manager.py +131 -71
  27. lamindb/models/_from_values.py +2 -2
  28. lamindb/models/_is_versioned.py +4 -4
  29. lamindb/models/_label_manager.py +4 -4
  30. lamindb/models/artifact.py +326 -172
  31. lamindb/models/artifact_set.py +45 -1
  32. lamindb/models/can_curate.py +1 -2
  33. lamindb/models/collection.py +3 -34
  34. lamindb/models/feature.py +111 -7
  35. lamindb/models/has_parents.py +11 -11
  36. lamindb/models/project.py +18 -0
  37. lamindb/models/query_manager.py +16 -7
  38. lamindb/models/query_set.py +191 -78
  39. lamindb/models/record.py +30 -5
  40. lamindb/models/run.py +10 -33
  41. lamindb/models/save.py +6 -8
  42. lamindb/models/schema.py +54 -26
  43. lamindb/models/sqlrecord.py +152 -40
  44. lamindb/models/storage.py +59 -14
  45. lamindb/models/transform.py +17 -17
  46. lamindb/models/ulabel.py +6 -1
  47. {lamindb-1.10.2.dist-info → lamindb-1.11.0.dist-info}/METADATA +12 -18
  48. {lamindb-1.10.2.dist-info → lamindb-1.11.0.dist-info}/RECORD +50 -47
  49. {lamindb-1.10.2.dist-info → lamindb-1.11.0.dist-info}/WHEEL +1 -1
  50. {lamindb-1.10.2.dist-info/licenses → lamindb-1.11.0.dist-info}/LICENSE +0 -0
lamindb/models/save.py CHANGED
@@ -47,11 +47,10 @@ def save(
47
47
 
48
48
  Args:
49
49
  records: Multiple :class:`~lamindb.models.SQLRecord` objects.
50
- ignore_conflicts: If ``True``, do not error if some records violate a
51
- unique or another constraint. However, it won't inplace update the id
52
- fields of records. If you need records with ids, you need to query
53
- them from the database.
54
- batch_size: Number of records to process in each batch. Defaults to 10000.
50
+ ignore_conflicts: If `True`, do not error if some records violate a unique or another constraint.
51
+ However, it won't inplace update the id fields of records.
52
+ If you need records with ids, you need to query them from the database.
53
+ batch_size: Number of records to process in each batch.
55
54
  Large batch sizes can improve performance but may lead to memory issues.
56
55
 
57
56
  Examples:
@@ -130,7 +129,7 @@ def bulk_create(
130
129
  Args:
131
130
  records: Iterable of SQLRecord objects to create
132
131
  ignore_conflicts: Whether to ignore conflicts during creation
133
- batch_size: Number of records to process in each batch. Defaults to 10000.
132
+ batch_size: Number of records to process in each batch.
134
133
  """
135
134
  records_by_orm = defaultdict(list)
136
135
  for record in records:
@@ -332,8 +331,7 @@ def store_artifacts(
332
331
  from .artifact import Artifact
333
332
 
334
333
  exception: Exception | None = None
335
- # because uploads might fail, we need to maintain a new list
336
- # of the succeeded uploads
334
+ # because uploads might fail, we need to maintain a new list of the succeeded uploads
337
335
  stored_artifacts = []
338
336
 
339
337
  # upload new local artifacts
lamindb/models/schema.py CHANGED
@@ -6,6 +6,7 @@ import numpy as np
6
6
  from django.db import models
7
7
  from django.db.models import CASCADE, PROTECT, ManyToManyField
8
8
  from lamin_utils import logger
9
+ from lamindb_setup.core import deprecated
9
10
  from lamindb_setup.core.hashing import HASH_LENGTH, hash_string
10
11
  from rich.table import Table
11
12
  from rich.text import Text
@@ -348,11 +349,12 @@ class Schema(SQLRecord, CanCurate, TracksRun):
348
349
 
349
350
  # from a dataframe
350
351
  df = pd.DataFrame({"feat1": [1, 2], "feat2": [3.1, 4.2], "feat3": ["cond1", "cond2"]})
351
- schema = ln.Schema.from_df(df)
352
+ schema = ln.Schema.from_dataframe(df)
352
353
  """
353
354
 
354
355
  class Meta(SQLRecord.Meta, TracksRun.Meta, TracksUpdates.Meta):
355
356
  abstract = False
357
+ app_label = "lamindb"
356
358
 
357
359
  _name_field: str = "name"
358
360
  _aux_fields: dict[str, tuple[str, type]] = {
@@ -576,19 +578,22 @@ class Schema(SQLRecord, CanCurate, TracksRun):
576
578
  self.optionals.set(optional_features)
577
579
  return None
578
580
  self._slots: dict[str, Schema] = {}
581
+
579
582
  if features:
580
583
  self._features = (get_related_name(features_registry), features) # type: ignore
581
- elif slots:
584
+ if slots:
582
585
  for slot_key, component in slots.items():
583
586
  if component._state.adding:
584
587
  raise InvalidArgument(
585
588
  f"schema for {slot_key} {component} must be saved before use"
586
589
  )
587
590
  self._slots = slots
591
+
588
592
  if validated_kwargs["hash"] in KNOWN_SCHEMAS:
589
593
  validated_kwargs["uid"] = KNOWN_SCHEMAS[validated_kwargs["hash"]]
590
594
  else:
591
595
  validated_kwargs["uid"] = ids.base62_16()
596
+
592
597
  super().__init__(**validated_kwargs)
593
598
 
594
599
  def _validate_kwargs_calculate_hash(
@@ -623,14 +628,20 @@ class Schema(SQLRecord, CanCurate, TracksRun):
623
628
  raise TypeError("index must be a Feature")
624
629
  features.insert(0, index)
625
630
 
631
+ if slots:
632
+ itype = "Composite"
633
+ if otype is None:
634
+ raise InvalidArgument("Please pass otype != None for composite schemas")
635
+
626
636
  if features:
627
637
  features, configs = get_features_config(features)
628
638
  features_registry = validate_features(features)
629
- itype_compare = features_registry.__get_name_with_module__()
630
- if itype is not None:
631
- assert itype.startswith(itype_compare), str(itype_compare) # noqa: S101
632
- else:
633
- itype = itype_compare
639
+ if itype != "Composite":
640
+ itype_compare = features_registry.__get_name_with_module__()
641
+ if itype is not None:
642
+ assert itype.startswith(itype_compare), str(itype_compare) # noqa: S101
643
+ else:
644
+ itype = itype_compare
634
645
  if n_features is not None:
635
646
  if n_features != len(features):
636
647
  logger.important(f"updating to n {len(features)} features")
@@ -654,11 +665,6 @@ class Schema(SQLRecord, CanCurate, TracksRun):
654
665
  if flexible is None:
655
666
  flexible = flexible_default
656
667
 
657
- if slots:
658
- itype = "Composite"
659
- if otype is None:
660
- raise InvalidArgument("Please pass otype != None for composite schemas")
661
-
662
668
  if itype is not None and not isinstance(itype, str):
663
669
  itype_str = serialize_dtype(itype, is_itype=True)
664
670
  else:
@@ -771,7 +777,7 @@ class Schema(SQLRecord, CanCurate, TracksRun):
771
777
  cls,
772
778
  values: ListLike,
773
779
  field: FieldAttr = Feature.name,
774
- type: str | None = None,
780
+ dtype: str | None = None,
775
781
  name: str | None = None,
776
782
  mute: bool = False,
777
783
  organism: SQLRecord | str | None = None,
@@ -783,7 +789,7 @@ class Schema(SQLRecord, CanCurate, TracksRun):
783
789
  Args:
784
790
  values: A list of values, like feature names or ids.
785
791
  field: The field of a reference registry to map values.
786
- type: The simple type.
792
+ dtype: The simple dtype.
787
793
  Defaults to `None` if reference registry is :class:`~lamindb.Feature`,
788
794
  defaults to `"float"` otherwise.
789
795
  name: A name.
@@ -816,8 +822,8 @@ class Schema(SQLRecord, CanCurate, TracksRun):
816
822
  if isinstance(values, DICT_KEYS_TYPE):
817
823
  values = list(values)
818
824
  registry = field.field.model
819
- if registry != Feature and type is None:
820
- type = NUMBER_TYPE
825
+ if registry != Feature and dtype is None:
826
+ dtype = NUMBER_TYPE
821
827
  logger.debug("setting feature set to 'number'")
822
828
  validated = registry.validate(values, field=field, mute=mute, organism=organism)
823
829
  values_array = np.array(values)
@@ -841,12 +847,12 @@ class Schema(SQLRecord, CanCurate, TracksRun):
841
847
  schema = Schema(
842
848
  features=validated_features,
843
849
  name=name,
844
- dtype=get_type_str(type),
850
+ dtype=get_type_str(dtype),
845
851
  )
846
852
  return schema
847
853
 
848
854
  @classmethod
849
- def from_df(
855
+ def from_dataframe(
850
856
  cls,
851
857
  df: pd.DataFrame,
852
858
  field: FieldAttr = Feature.name,
@@ -889,15 +895,28 @@ class Schema(SQLRecord, CanCurate, TracksRun):
889
895
  )
890
896
  return schema
891
897
 
898
+ @classmethod
899
+ @deprecated("from_dataframe")
900
+ def from_df(
901
+ cls,
902
+ df: pd.DataFrame,
903
+ field: FieldAttr = Feature.name,
904
+ name: str | None = None,
905
+ mute: bool = False,
906
+ organism: SQLRecord | str | None = None,
907
+ source: SQLRecord | None = None,
908
+ ) -> Schema | None:
909
+ return cls.from_dataframe(df, field, name, mute, organism, source)
910
+
892
911
  def save(self, *args, **kwargs) -> Schema:
893
- """Save."""
912
+ """Save schema."""
894
913
  from .save import bulk_create
895
914
 
896
915
  if self.pk is not None:
897
916
  features = (
898
917
  self._features[1]
899
918
  if hasattr(self, "_features")
900
- else (self.members.list() if self.members.exists() else [])
919
+ else (self.members.to_list() if self.members.exists() else [])
901
920
  )
902
921
  index_feature = self.index
903
922
  _, validated_kwargs, _, _, _ = self._validate_kwargs_calculate_hash(
@@ -925,7 +944,7 @@ class Schema(SQLRecord, CanCurate, TracksRun):
925
944
  datasets = Artifact.filter(schema=self).all()
926
945
  if datasets.exists():
927
946
  logger.warning(
928
- f"you updated the schema hash and might invalidate datasets that were previously validated with this schema: {datasets.list('uid')}"
947
+ f"you updated the schema hash and might invalidate datasets that were previously validated with this schema: {datasets.to_list('uid')}"
929
948
  )
930
949
  self.hash = validated_kwargs["hash"]
931
950
  self.n = validated_kwargs["n"]
@@ -947,13 +966,16 @@ class Schema(SQLRecord, CanCurate, TracksRun):
947
966
  assert self.n > 0 # noqa: S101
948
967
  using: bool | None = kwargs.pop("using", None)
949
968
  related_name, records = self._features
969
+
970
+ # .set() does not preserve the order but orders by the feature primary key
950
971
  # only the following method preserves the order
951
- # .set() does not preserve the order but orders by
952
- # the feature primary key
953
972
  through_model = getattr(self, related_name).through
954
- related_model_split = parse_cat_dtype(self.itype, is_itype=True)[
955
- "registry_str"
956
- ].split(".")
973
+ if self.itype == "Composite":
974
+ related_model_split = ["Feature"]
975
+ else:
976
+ related_model_split = parse_cat_dtype(self.itype, is_itype=True)[
977
+ "registry_str"
978
+ ].split(".")
957
979
  if len(related_model_split) == 1:
958
980
  related_field = related_model_split[0].lower()
959
981
  else:
@@ -965,6 +987,7 @@ class Schema(SQLRecord, CanCurate, TracksRun):
965
987
  ]
966
988
  through_model.objects.using(using).bulk_create(links, ignore_conflicts=True)
967
989
  delattr(self, "_features")
990
+
968
991
  return self
969
992
 
970
993
  @property
@@ -978,6 +1001,8 @@ class Schema(SQLRecord, CanCurate, TracksRun):
978
1001
  # this should return a queryset and not a list...
979
1002
  # need to fix this
980
1003
  return self._features[1]
1004
+ if len(self.features.all()) > 0:
1005
+ return self.features.order_by("links_schema__id")
981
1006
  if self.itype == "Composite" or self.is_type:
982
1007
  return Feature.objects.none()
983
1008
  related_name = self._get_related_name()
@@ -1200,6 +1225,7 @@ class SchemaFeature(BaseSQLRecord, IsLink):
1200
1225
  feature: Feature = ForeignKey(Feature, PROTECT, related_name="links_schema")
1201
1226
 
1202
1227
  class Meta:
1228
+ app_label = "lamindb"
1203
1229
  unique_together = ("schema", "feature")
1204
1230
 
1205
1231
 
@@ -1211,6 +1237,7 @@ class ArtifactSchema(BaseSQLRecord, IsLink, TracksRun):
1211
1237
  feature_ref_is_semantic: bool | None = BooleanField(null=True)
1212
1238
 
1213
1239
  class Meta:
1240
+ app_label = "lamindb"
1214
1241
  unique_together = (("artifact", "schema"), ("artifact", "slot"))
1215
1242
 
1216
1243
 
@@ -1221,6 +1248,7 @@ class SchemaComponent(BaseSQLRecord, IsLink, TracksRun):
1221
1248
  slot: str | None = CharField(null=True)
1222
1249
 
1223
1250
  class Meta:
1251
+ app_label = "lamindb"
1224
1252
  unique_together = (("composite", "slot", "component"), ("composite", "slot"))
1225
1253
 
1226
1254
 
@@ -319,6 +319,43 @@ def suggest_records_with_similar_names(
319
319
  return None
320
320
 
321
321
 
322
+ def delete_record(record: BaseSQLRecord, is_soft: bool = True):
323
+ def delete():
324
+ if is_soft:
325
+ record.branch_id = -1
326
+ record.save()
327
+ else:
328
+ super(BaseSQLRecord, record).delete()
329
+
330
+ # deal with versioned records
331
+ # if _ovewrite_version = True, there is only a single version and
332
+ # no need to set the new latest version because all versions are deleted
333
+ # when deleting the latest version
334
+ if (
335
+ isinstance(record, IsVersioned)
336
+ and record.is_latest
337
+ and not getattr(record, "_overwrite_versions", False)
338
+ ):
339
+ new_latest = (
340
+ record.__class__.objects.using(record._state.db)
341
+ .filter(is_latest=False, uid__startswith=record.stem_uid)
342
+ .exclude(branch_id=-1) # exclude candidates in the trash
343
+ .order_by("-created_at")
344
+ .first()
345
+ )
346
+ if new_latest is not None:
347
+ new_latest.is_latest = True
348
+ if is_soft:
349
+ record.is_latest = False
350
+ with transaction.atomic():
351
+ new_latest.save()
352
+ delete()
353
+ logger.warning(f"new latest version is: {new_latest}")
354
+ return None
355
+ # deal with all other cases of the nested if condition now
356
+ delete()
357
+
358
+
322
359
  RECORD_REGISTRY_EXAMPLE = """Example::
323
360
 
324
361
  from lamindb import SQLRecord, fields
@@ -334,7 +371,7 @@ RECORD_REGISTRY_EXAMPLE = """Example::
334
371
  experiment.save()
335
372
 
336
373
  # `Experiment` refers to the registry, which you can query
337
- df = Experiment.filter(name__startswith="my ").df()
374
+ df = Experiment.filter(name__startswith="my ").to_dataframe()
338
375
  """
339
376
 
340
377
 
@@ -425,7 +462,7 @@ class Registry(ModelBase):
425
462
 
426
463
  Examples:
427
464
  >>> ln.ULabel(name="my label").save()
428
- >>> ln.ULabel.filter(name__startswith="my").df()
465
+ >>> ln.ULabel.filter(name__startswith="my").to_dataframe()
429
466
  """
430
467
  from .query_set import QuerySet
431
468
 
@@ -464,7 +501,7 @@ class Registry(ModelBase):
464
501
 
465
502
  return QuerySet(model=cls).get(idlike, **expressions)
466
503
 
467
- def df(
504
+ def to_dataframe(
468
505
  cls,
469
506
  include: str | list[str] | None = None,
470
507
  features: bool | list[str] | str = False,
@@ -492,21 +529,30 @@ class Registry(ModelBase):
492
529
 
493
530
  Include the name of the creator in the `DataFrame`:
494
531
 
495
- >>> ln.ULabel.df(include="created_by__name"])
532
+ >>> ln.ULabel.to_dataframe(include="created_by__name"])
496
533
 
497
534
  Include display of features for `Artifact`:
498
535
 
499
- >>> df = ln.Artifact.df(features=True)
536
+ >>> df = ln.Artifact.to_dataframe(features=True)
500
537
  >>> ln.view(df) # visualize with type annotations
501
538
 
502
539
  Only include select features:
503
540
 
504
- >>> df = ln.Artifact.df(features=["cell_type_by_expert", "cell_type_by_model"])
541
+ >>> df = ln.Artifact.to_dataframe(features=["cell_type_by_expert", "cell_type_by_model"])
505
542
  """
506
543
  query_set = cls.filter()
507
544
  if hasattr(cls, "updated_at"):
508
545
  query_set = query_set.order_by("-updated_at")
509
- return query_set[:limit].df(include=include, features=features)
546
+ return query_set[:limit].to_dataframe(include=include, features=features)
547
+
548
+ @deprecated(new_name="to_dataframe")
549
+ def df(
550
+ cls,
551
+ include: str | list[str] | None = None,
552
+ features: bool | list[str] | str = False,
553
+ limit: int = 100,
554
+ ) -> pd.DataFrame:
555
+ return cls.to_dataframe(include, features, limit)
510
556
 
511
557
  @doc_args(_search.__doc__)
512
558
  def search(
@@ -580,7 +626,7 @@ class Registry(ModelBase):
580
626
  # this just retrives the full connection string from iresult
581
627
  db = update_db_using_local(iresult, settings_file)
582
628
  cache_using_filepath.write_text(
583
- f"{iresult['lnid']}\n{iresult['schema_str']}"
629
+ f"{iresult['lnid']}\n{iresult['schema_str']}", encoding="utf-8"
584
630
  )
585
631
  # need to set the token if it is a fine_grained_access and the user is jwt (not public)
586
632
  is_fine_grained_access = (
@@ -593,7 +639,7 @@ class Registry(ModelBase):
593
639
  source_modules = isettings.modules
594
640
  db = isettings.db
595
641
  cache_using_filepath.write_text(
596
- f"{isettings.uid}\n{','.join(source_modules)}"
642
+ f"{isettings.uid}\n{','.join(source_modules)}", encoding="utf-8"
597
643
  )
598
644
  # need to set the token if it is a fine_grained_access and the user is jwt (not public)
599
645
  is_fine_grained_access = (
@@ -795,7 +841,7 @@ class BaseSQLRecord(models.Model, metaclass=Registry):
795
841
  artifacts: list = []
796
842
  if self.__class__.__name__ == "Collection" and self.id is not None:
797
843
  # when creating a new collection without being able to access artifacts
798
- artifacts = self.ordered_artifacts.list()
844
+ artifacts = self.ordered_artifacts.to_list()
799
845
  pre_existing_record = None
800
846
  # consider records that are being transferred from other databases
801
847
  transfer_logs: dict[str, list[str]] = {
@@ -920,27 +966,7 @@ class BaseSQLRecord(models.Model, metaclass=Registry):
920
966
 
921
967
  def delete(self) -> None:
922
968
  """Delete."""
923
- # note that the logic below does not fire if a record is moved to the trash
924
- # the idea is that moving a record to the trash should move its entire version family
925
- # to the trash, whereas permanently deleting should default to only deleting a single record
926
- # of a version family
927
- # we can consider making it easy to permanently delete entire version families as well,
928
- # but that's for another time
929
- if isinstance(self, IsVersioned) and self.is_latest:
930
- new_latest = (
931
- self.__class__.objects.using(self._state.db)
932
- .filter(is_latest=False, uid__startswith=self.stem_uid)
933
- .order_by("-created_at")
934
- .first()
935
- )
936
- if new_latest is not None:
937
- new_latest.is_latest = True
938
- with transaction.atomic():
939
- new_latest.save()
940
- super().delete() # type: ignore
941
- logger.warning(f"new latest version is {new_latest}")
942
- return None
943
- super().delete()
969
+ delete_record(self, is_soft=False)
944
970
 
945
971
 
946
972
  class Space(BaseSQLRecord):
@@ -952,6 +978,7 @@ class Space(BaseSQLRecord):
952
978
  """
953
979
 
954
980
  class Meta:
981
+ app_label = "lamindb"
955
982
  constraints = [
956
983
  models.UniqueConstraint(Lower("name"), name="unique_space_name_lower")
957
984
  ]
@@ -964,8 +991,7 @@ class Space(BaseSQLRecord):
964
991
  editable=False,
965
992
  unique=True,
966
993
  max_length=12,
967
- default="aaaaaaaaaaaaa",
968
- db_default="aaaaaaaaaaaa",
994
+ default=base62_12,
969
995
  db_index=True,
970
996
  )
971
997
  """Universal id."""
@@ -998,6 +1024,21 @@ class Space(BaseSQLRecord):
998
1024
  *args,
999
1025
  **kwargs,
1000
1026
  ):
1027
+ if not args and "uid" not in kwargs:
1028
+ warn = False
1029
+ msg = ""
1030
+ isettings = setup_settings.instance
1031
+ if (dialect := isettings.dialect) != "postgresql":
1032
+ warn = True
1033
+ msg = f"on {dialect} databases"
1034
+ elif not isettings.is_on_hub:
1035
+ warn = True
1036
+ msg = "on local instances"
1037
+ if warn:
1038
+ logger.warning(
1039
+ f"creating spaces manually {msg} is possible for demo purposes, "
1040
+ "but does *not* affect access permissions"
1041
+ )
1001
1042
  super().__init__(*args, **kwargs)
1002
1043
 
1003
1044
 
@@ -1007,6 +1048,12 @@ class Branch(BaseSQLRecord):
1007
1048
  Every `SQLRecord` has a `branch` field, which dictates where a record appears in queries & searches.
1008
1049
  """
1009
1050
 
1051
+ class Meta:
1052
+ app_label = "lamindb"
1053
+ constraints = [
1054
+ models.UniqueConstraint(Lower("name"), name="unique_branch_name_lower")
1055
+ ]
1056
+
1010
1057
  # below isn't fully implemented but a roadmap
1011
1058
  # - 3: template (hidden in queries & searches)
1012
1059
  # - 2: locked (same as default, but locked for edits except for space admins)
@@ -1018,11 +1065,6 @@ class Branch(BaseSQLRecord):
1018
1065
  # that can be merged onto the main branch in an experience akin to a Pull Request. The mapping
1019
1066
  # onto a semantic branch name is handled through LaminHub.
1020
1067
 
1021
- class Meta:
1022
- constraints = [
1023
- models.UniqueConstraint(Lower("name"), name="unique_branch_name_lower")
1024
- ]
1025
-
1026
1068
  id: int = models.AutoField(primary_key=True)
1027
1069
  """An integer id that's synchronized for a family of coupled database instances.
1028
1070
 
@@ -1119,6 +1161,75 @@ class SQLRecord(BaseSQLRecord, metaclass=Registry):
1119
1161
  def _branch_code(self, value: int):
1120
1162
  self.branch_id = value
1121
1163
 
1164
+ def delete(self, permanent: bool | None = None, **kwargs) -> None:
1165
+ """Delete record.
1166
+
1167
+ Args:
1168
+ permanent: Whether to permanently delete the record (skips trash).
1169
+
1170
+ Examples:
1171
+
1172
+ For any `SQLRecord` object `record`, call:
1173
+
1174
+ >>> record.delete()
1175
+ """
1176
+ if self._state.adding:
1177
+ logger.warning("record is not yet saved, delete has no effect")
1178
+ return
1179
+ name_with_module = self.__class__.__get_name_with_module__()
1180
+
1181
+ if name_with_module == "Artifact":
1182
+ # this first check means an invalid delete fails fast rather than cascading through
1183
+ # database and storage permission errors
1184
+ isettings = setup_settings.instance
1185
+ if self.storage.instance_uid != isettings.uid and (
1186
+ kwargs["storage"] or kwargs["storage"] is None
1187
+ ):
1188
+ from ..errors import IntegrityError
1189
+ from .storage import Storage
1190
+
1191
+ raise IntegrityError(
1192
+ "Cannot simply delete artifacts outside of this instance's managed storage locations."
1193
+ "\n(1) If you only want to delete the metadata record in this instance, pass `storage=False`"
1194
+ f"\n(2) If you want to delete the artifact in storage, please load the managing lamindb instance (uid={self.storage.instance_uid})."
1195
+ f"\nThese are all managed storage locations of this instance:\n{Storage.filter(instance_uid=isettings.uid).to_dataframe()}"
1196
+ )
1197
+
1198
+ # change branch_id to trash
1199
+ trash_branch_id = -1
1200
+ if self.branch_id > trash_branch_id and permanent is not True:
1201
+ delete_record(self, is_soft=True)
1202
+ logger.warning(f"moved record to trash (branch_id = -1): {self}")
1203
+ return
1204
+
1205
+ # permanent delete
1206
+ if permanent is None:
1207
+ response = input(
1208
+ f"Record {self.uid} is already in trash! Are you sure you want to delete it from your"
1209
+ " database? You can't undo this action. (y/n) "
1210
+ )
1211
+ confirm_delete = response == "y"
1212
+ else:
1213
+ confirm_delete = permanent
1214
+
1215
+ if confirm_delete:
1216
+ if name_with_module == "Run":
1217
+ from .run import delete_run_artifacts
1218
+
1219
+ delete_run_artifacts(self)
1220
+ elif name_with_module == "Transform":
1221
+ from .transform import delete_transform_relations
1222
+
1223
+ delete_transform_relations(self)
1224
+ elif name_with_module == "Artifact":
1225
+ from .artifact import delete_permanently
1226
+
1227
+ delete_permanently(
1228
+ self, storage=kwargs["storage"], using_key=kwargs["using_key"]
1229
+ )
1230
+ if name_with_module != "Artifact":
1231
+ super().delete()
1232
+
1122
1233
 
1123
1234
  def _format_django_validation_error(record: SQLRecord, e: DjangoValidationError):
1124
1235
  """Pretty print Django validation errors."""
@@ -1464,7 +1575,7 @@ def check_name_change(record: SQLRecord):
1464
1575
  .exclude(feature_id=None) # must have a feature
1465
1576
  .distinct()
1466
1577
  )
1467
- artifact_ids = linked_records.list("artifact__uid")
1578
+ artifact_ids = linked_records.to_list("artifact__uid")
1468
1579
  n = len(artifact_ids)
1469
1580
  if n > 0:
1470
1581
  s = "s" if n > 1 else ""
@@ -1482,7 +1593,7 @@ def check_name_change(record: SQLRecord):
1482
1593
  # when a feature is renamed
1483
1594
  elif isinstance(record, Feature):
1484
1595
  # only internal features are associated with schemas
1485
- linked_artifacts = Artifact.filter(feature_sets__features=record).list(
1596
+ linked_artifacts = Artifact.filter(feature_sets__features=record).to_list(
1486
1597
  "uid"
1487
1598
  )
1488
1599
  n = len(linked_artifacts)
@@ -1806,6 +1917,7 @@ class Migration(BaseSQLRecord):
1806
1917
 
1807
1918
  class Meta:
1808
1919
  db_table = "django_migrations"
1920
+ app_label = "lamindb"
1809
1921
  managed = False
1810
1922
 
1811
1923