lamindb 1.10.1__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.
- lamindb/__init__.py +89 -49
- lamindb/_finish.py +17 -15
- lamindb/_tracked.py +2 -4
- lamindb/_view.py +1 -1
- lamindb/base/__init__.py +2 -1
- lamindb/base/dtypes.py +76 -0
- lamindb/core/_settings.py +45 -2
- lamindb/core/storage/_anndata_accessor.py +118 -26
- lamindb/core/storage/_backed_access.py +10 -7
- lamindb/core/storage/_spatialdata_accessor.py +15 -4
- lamindb/core/storage/_zarr.py +3 -0
- lamindb/curators/_legacy.py +16 -3
- lamindb/curators/core.py +449 -193
- lamindb/errors.py +6 -0
- lamindb/examples/cellxgene/__init__.py +8 -3
- lamindb/examples/cellxgene/_cellxgene.py +127 -13
- lamindb/examples/cellxgene/{cxg_schema_versions.csv → cellxgene_schema_versions.csv} +11 -0
- lamindb/examples/croissant/__init__.py +32 -6
- lamindb/examples/datasets/__init__.py +2 -2
- lamindb/examples/datasets/_core.py +9 -2
- lamindb/examples/datasets/_small.py +66 -22
- lamindb/examples/fixtures/sheets.py +8 -2
- lamindb/integrations/_croissant.py +34 -11
- lamindb/migrations/0118_alter_recordproject_value_projectrecord.py +99 -0
- lamindb/migrations/0119_rename_records_project_linked_in_records.py +26 -0
- lamindb/migrations/{0117_squashed.py → 0119_squashed.py} +92 -5
- lamindb/migrations/0120_add_record_fk_constraint.py +64 -0
- lamindb/migrations/0121_recorduser.py +60 -0
- lamindb/models/__init__.py +4 -1
- lamindb/models/_describe.py +2 -2
- lamindb/models/_feature_manager.py +131 -71
- lamindb/models/_from_values.py +2 -2
- lamindb/models/_is_versioned.py +4 -4
- lamindb/models/_label_manager.py +4 -4
- lamindb/models/artifact.py +357 -192
- lamindb/models/artifact_set.py +45 -1
- lamindb/models/can_curate.py +1 -2
- lamindb/models/collection.py +3 -34
- lamindb/models/feature.py +111 -7
- lamindb/models/has_parents.py +11 -11
- lamindb/models/project.py +42 -2
- lamindb/models/query_manager.py +16 -7
- lamindb/models/query_set.py +191 -78
- lamindb/models/record.py +30 -5
- lamindb/models/run.py +10 -33
- lamindb/models/save.py +6 -8
- lamindb/models/schema.py +54 -26
- lamindb/models/sqlrecord.py +152 -40
- lamindb/models/storage.py +59 -14
- lamindb/models/transform.py +17 -17
- lamindb/models/ulabel.py +6 -1
- {lamindb-1.10.1.dist-info → lamindb-1.11.0.dist-info}/METADATA +11 -16
- {lamindb-1.10.1.dist-info → lamindb-1.11.0.dist-info}/RECORD +55 -50
- {lamindb-1.10.1.dist-info → lamindb-1.11.0.dist-info}/LICENSE +0 -0
- {lamindb-1.10.1.dist-info → lamindb-1.11.0.dist-info}/WHEEL +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
|
51
|
-
|
52
|
-
|
53
|
-
|
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.
|
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.
|
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
|
-
|
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
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
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
|
-
|
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
|
-
|
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
|
820
|
-
|
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(
|
850
|
+
dtype=get_type_str(dtype),
|
845
851
|
)
|
846
852
|
return schema
|
847
853
|
|
848
854
|
@classmethod
|
849
|
-
def
|
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.
|
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.
|
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
|
-
|
955
|
-
"
|
956
|
-
|
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
|
|
lamindb/models/sqlrecord.py
CHANGED
@@ -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 ").
|
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").
|
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
|
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.
|
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.
|
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.
|
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].
|
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.
|
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
|
-
|
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=
|
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.
|
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).
|
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
|
|