lamindb 1.10.2__py3-none-any.whl → 1.11a1__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 (47) hide show
  1. lamindb/__init__.py +89 -49
  2. lamindb/_finish.py +14 -12
  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 +432 -186
  11. lamindb/examples/cellxgene/__init__.py +8 -3
  12. lamindb/examples/cellxgene/_cellxgene.py +127 -13
  13. lamindb/examples/cellxgene/{cxg_schema_versions.csv → cellxgene_schema_versions.csv} +11 -0
  14. lamindb/examples/croissant/__init__.py +12 -2
  15. lamindb/examples/datasets/__init__.py +2 -2
  16. lamindb/examples/datasets/_core.py +1 -1
  17. lamindb/examples/datasets/_small.py +66 -22
  18. lamindb/examples/datasets/mini_immuno.py +1 -0
  19. lamindb/migrations/0119_squashed.py +5 -2
  20. lamindb/migrations/0120_add_record_fk_constraint.py +64 -0
  21. lamindb/migrations/0121_recorduser.py +53 -0
  22. lamindb/models/__init__.py +3 -1
  23. lamindb/models/_describe.py +2 -2
  24. lamindb/models/_feature_manager.py +53 -53
  25. lamindb/models/_from_values.py +2 -2
  26. lamindb/models/_is_versioned.py +4 -4
  27. lamindb/models/_label_manager.py +4 -4
  28. lamindb/models/artifact.py +305 -116
  29. lamindb/models/artifact_set.py +36 -1
  30. lamindb/models/can_curate.py +1 -2
  31. lamindb/models/collection.py +3 -34
  32. lamindb/models/feature.py +111 -7
  33. lamindb/models/has_parents.py +11 -11
  34. lamindb/models/project.py +18 -0
  35. lamindb/models/query_manager.py +16 -7
  36. lamindb/models/query_set.py +59 -34
  37. lamindb/models/record.py +25 -4
  38. lamindb/models/run.py +8 -6
  39. lamindb/models/schema.py +54 -26
  40. lamindb/models/sqlrecord.py +123 -25
  41. lamindb/models/storage.py +59 -14
  42. lamindb/models/transform.py +17 -17
  43. lamindb/models/ulabel.py +6 -1
  44. {lamindb-1.10.2.dist-info → lamindb-1.11a1.dist-info}/METADATA +4 -5
  45. {lamindb-1.10.2.dist-info → lamindb-1.11a1.dist-info}/RECORD +47 -44
  46. {lamindb-1.10.2.dist-info → lamindb-1.11a1.dist-info}/WHEEL +1 -1
  47. {lamindb-1.10.2.dist-info/licenses → lamindb-1.11a1.dist-info}/LICENSE +0 -0
@@ -334,7 +334,7 @@ RECORD_REGISTRY_EXAMPLE = """Example::
334
334
  experiment.save()
335
335
 
336
336
  # `Experiment` refers to the registry, which you can query
337
- df = Experiment.filter(name__startswith="my ").df()
337
+ df = Experiment.filter(name__startswith="my ").to_dataframe()
338
338
  """
339
339
 
340
340
 
@@ -425,7 +425,7 @@ class Registry(ModelBase):
425
425
 
426
426
  Examples:
427
427
  >>> ln.ULabel(name="my label").save()
428
- >>> ln.ULabel.filter(name__startswith="my").df()
428
+ >>> ln.ULabel.filter(name__startswith="my").to_dataframe()
429
429
  """
430
430
  from .query_set import QuerySet
431
431
 
@@ -464,7 +464,7 @@ class Registry(ModelBase):
464
464
 
465
465
  return QuerySet(model=cls).get(idlike, **expressions)
466
466
 
467
- def df(
467
+ def to_dataframe(
468
468
  cls,
469
469
  include: str | list[str] | None = None,
470
470
  features: bool | list[str] | str = False,
@@ -492,21 +492,30 @@ class Registry(ModelBase):
492
492
 
493
493
  Include the name of the creator in the `DataFrame`:
494
494
 
495
- >>> ln.ULabel.df(include="created_by__name"])
495
+ >>> ln.ULabel.to_dataframe(include="created_by__name"])
496
496
 
497
497
  Include display of features for `Artifact`:
498
498
 
499
- >>> df = ln.Artifact.df(features=True)
499
+ >>> df = ln.Artifact.to_dataframe(features=True)
500
500
  >>> ln.view(df) # visualize with type annotations
501
501
 
502
502
  Only include select features:
503
503
 
504
- >>> df = ln.Artifact.df(features=["cell_type_by_expert", "cell_type_by_model"])
504
+ >>> df = ln.Artifact.to_dataframe(features=["cell_type_by_expert", "cell_type_by_model"])
505
505
  """
506
506
  query_set = cls.filter()
507
507
  if hasattr(cls, "updated_at"):
508
508
  query_set = query_set.order_by("-updated_at")
509
- return query_set[:limit].df(include=include, features=features)
509
+ return query_set[:limit].to_dataframe(include=include, features=features)
510
+
511
+ @deprecated(new_name="to_dataframe")
512
+ def df(
513
+ cls,
514
+ include: str | list[str] | None = None,
515
+ features: bool | list[str] | str = False,
516
+ limit: int = 100,
517
+ ) -> pd.DataFrame:
518
+ return cls.to_dataframe(include, features, limit)
510
519
 
511
520
  @doc_args(_search.__doc__)
512
521
  def search(
@@ -795,7 +804,7 @@ class BaseSQLRecord(models.Model, metaclass=Registry):
795
804
  artifacts: list = []
796
805
  if self.__class__.__name__ == "Collection" and self.id is not None:
797
806
  # when creating a new collection without being able to access artifacts
798
- artifacts = self.ordered_artifacts.list()
807
+ artifacts = self.ordered_artifacts.to_list()
799
808
  pre_existing_record = None
800
809
  # consider records that are being transferred from other databases
801
810
  transfer_logs: dict[str, list[str]] = {
@@ -920,13 +929,15 @@ class BaseSQLRecord(models.Model, metaclass=Registry):
920
929
 
921
930
  def delete(self) -> None:
922
931
  """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:
932
+ # deal with versioned records
933
+ # _overwrite_versions is set to True for folder artifacts
934
+ # no need to set the new latest version becase all versions are deleted
935
+ # when deleting the latest version of a folder artifact
936
+ if (
937
+ isinstance(self, IsVersioned)
938
+ and self.is_latest
939
+ and not getattr(self, "_overwrite_versions", False)
940
+ ):
930
941
  new_latest = (
931
942
  self.__class__.objects.using(self._state.db)
932
943
  .filter(is_latest=False, uid__startswith=self.stem_uid)
@@ -938,7 +949,7 @@ class BaseSQLRecord(models.Model, metaclass=Registry):
938
949
  with transaction.atomic():
939
950
  new_latest.save()
940
951
  super().delete() # type: ignore
941
- logger.warning(f"new latest version is {new_latest}")
952
+ logger.warning(f"new latest version is: {new_latest}")
942
953
  return None
943
954
  super().delete()
944
955
 
@@ -952,6 +963,7 @@ class Space(BaseSQLRecord):
952
963
  """
953
964
 
954
965
  class Meta:
966
+ app_label = "lamindb"
955
967
  constraints = [
956
968
  models.UniqueConstraint(Lower("name"), name="unique_space_name_lower")
957
969
  ]
@@ -964,8 +976,7 @@ class Space(BaseSQLRecord):
964
976
  editable=False,
965
977
  unique=True,
966
978
  max_length=12,
967
- default="aaaaaaaaaaaaa",
968
- db_default="aaaaaaaaaaaa",
979
+ default=base62_12,
969
980
  db_index=True,
970
981
  )
971
982
  """Universal id."""
@@ -998,6 +1009,21 @@ class Space(BaseSQLRecord):
998
1009
  *args,
999
1010
  **kwargs,
1000
1011
  ):
1012
+ if "uid" not in kwargs:
1013
+ warn = False
1014
+ msg = ""
1015
+ isettings = setup_settings.instance
1016
+ if (dialect := isettings.dialect) != "postgresql":
1017
+ warn = True
1018
+ msg = f"on {dialect} databases"
1019
+ elif not isettings.is_on_hub:
1020
+ warn = True
1021
+ msg = "on local instances"
1022
+ if warn:
1023
+ logger.warning(
1024
+ f"creating spaces manually {msg} is possible for demo purposes, "
1025
+ "but does *not* affect access permissions"
1026
+ )
1001
1027
  super().__init__(*args, **kwargs)
1002
1028
 
1003
1029
 
@@ -1007,6 +1033,12 @@ class Branch(BaseSQLRecord):
1007
1033
  Every `SQLRecord` has a `branch` field, which dictates where a record appears in queries & searches.
1008
1034
  """
1009
1035
 
1036
+ class Meta:
1037
+ app_label = "lamindb"
1038
+ constraints = [
1039
+ models.UniqueConstraint(Lower("name"), name="unique_branch_name_lower")
1040
+ ]
1041
+
1010
1042
  # below isn't fully implemented but a roadmap
1011
1043
  # - 3: template (hidden in queries & searches)
1012
1044
  # - 2: locked (same as default, but locked for edits except for space admins)
@@ -1018,11 +1050,6 @@ class Branch(BaseSQLRecord):
1018
1050
  # that can be merged onto the main branch in an experience akin to a Pull Request. The mapping
1019
1051
  # onto a semantic branch name is handled through LaminHub.
1020
1052
 
1021
- class Meta:
1022
- constraints = [
1023
- models.UniqueConstraint(Lower("name"), name="unique_branch_name_lower")
1024
- ]
1025
-
1026
1053
  id: int = models.AutoField(primary_key=True)
1027
1054
  """An integer id that's synchronized for a family of coupled database instances.
1028
1055
 
@@ -1119,6 +1146,76 @@ class SQLRecord(BaseSQLRecord, metaclass=Registry):
1119
1146
  def _branch_code(self, value: int):
1120
1147
  self.branch_id = value
1121
1148
 
1149
+ def delete(self, permanent: bool | None = None, **kwargs) -> None:
1150
+ """Delete record.
1151
+
1152
+ Args:
1153
+ permanent: Whether to permanently delete the record (skips trash).
1154
+
1155
+ Examples:
1156
+
1157
+ For any `SQLRecord` object `record`, call:
1158
+
1159
+ >>> record.delete()
1160
+ """
1161
+ if self._state.adding:
1162
+ logger.warning("record is not yet saved, delete has no effect")
1163
+ return
1164
+ name_with_module = self.__class__.__get_name_with_module__()
1165
+
1166
+ if name_with_module == "Artifact":
1167
+ # this first check means an invalid delete fails fast rather than cascading through
1168
+ # database and storage permission errors
1169
+ isettings = setup_settings.instance
1170
+ if self.storage.instance_uid != isettings.uid and (
1171
+ kwargs["storage"] or kwargs["storage"] is None
1172
+ ):
1173
+ from ..errors import IntegrityError
1174
+ from .storage import Storage
1175
+
1176
+ raise IntegrityError(
1177
+ "Cannot simply delete artifacts outside of this instance's managed storage locations."
1178
+ "\n(1) If you only want to delete the metadata record in this instance, pass `storage=False`"
1179
+ f"\n(2) If you want to delete the artifact in storage, please load the managing lamindb instance (uid={self.storage.instance_uid})."
1180
+ f"\nThese are all managed storage locations of this instance:\n{Storage.filter(instance_uid=isettings.uid).to_dataframe()}"
1181
+ )
1182
+
1183
+ # change branch_id to trash
1184
+ trash_branch_id = -1
1185
+ if self.branch_id > trash_branch_id and permanent is not True:
1186
+ self.branch_id = trash_branch_id
1187
+ self.save()
1188
+ logger.warning(f"moved record to trash (`branch_id = -1`): {self}")
1189
+ return
1190
+
1191
+ # permanent delete
1192
+ if permanent is None:
1193
+ response = input(
1194
+ f"Record {self.uid} is already in trash! Are you sure you want to delete it from your"
1195
+ " database? You can't undo this action. (y/n) "
1196
+ )
1197
+ delete_record = response == "y"
1198
+ else:
1199
+ delete_record = permanent
1200
+
1201
+ if delete_record:
1202
+ if name_with_module == "Run":
1203
+ from .run import delete_run_artifacts
1204
+
1205
+ delete_run_artifacts(self)
1206
+ elif name_with_module == "Transform":
1207
+ from .transform import delete_transform_relations
1208
+
1209
+ delete_transform_relations(self)
1210
+ elif name_with_module == "Artifact":
1211
+ from .artifact import delete_permanently
1212
+
1213
+ delete_permanently(
1214
+ self, storage=kwargs["storage"], using_key=kwargs["using_key"]
1215
+ )
1216
+ if name_with_module != "Artifact":
1217
+ super().delete()
1218
+
1122
1219
 
1123
1220
  def _format_django_validation_error(record: SQLRecord, e: DjangoValidationError):
1124
1221
  """Pretty print Django validation errors."""
@@ -1464,7 +1561,7 @@ def check_name_change(record: SQLRecord):
1464
1561
  .exclude(feature_id=None) # must have a feature
1465
1562
  .distinct()
1466
1563
  )
1467
- artifact_ids = linked_records.list("artifact__uid")
1564
+ artifact_ids = linked_records.to_list("artifact__uid")
1468
1565
  n = len(artifact_ids)
1469
1566
  if n > 0:
1470
1567
  s = "s" if n > 1 else ""
@@ -1482,7 +1579,7 @@ def check_name_change(record: SQLRecord):
1482
1579
  # when a feature is renamed
1483
1580
  elif isinstance(record, Feature):
1484
1581
  # only internal features are associated with schemas
1485
- linked_artifacts = Artifact.filter(feature_sets__features=record).list(
1582
+ linked_artifacts = Artifact.filter(feature_sets__features=record).to_list(
1486
1583
  "uid"
1487
1584
  )
1488
1585
  n = len(linked_artifacts)
@@ -1806,6 +1903,7 @@ class Migration(BaseSQLRecord):
1806
1903
 
1807
1904
  class Meta:
1808
1905
  db_table = "django_migrations"
1906
+ app_label = "lamindb"
1809
1907
  managed = False
1810
1908
 
1811
1909
 
lamindb/models/storage.py CHANGED
@@ -4,6 +4,7 @@ from typing import (
4
4
  TYPE_CHECKING,
5
5
  overload,
6
6
  )
7
+ from uuid import UUID
7
8
 
8
9
  from django.db import models
9
10
  from lamin_utils import logger
@@ -11,6 +12,8 @@ from lamindb_setup import settings as setup_settings
11
12
  from lamindb_setup.core._hub_core import (
12
13
  delete_storage_record,
13
14
  get_storage_records_for_instance,
15
+ select_space,
16
+ update_storage_with_space,
14
17
  )
15
18
  from lamindb_setup.core._settings_storage import (
16
19
  StorageSettings,
@@ -25,7 +28,7 @@ from lamindb.base.fields import (
25
28
 
26
29
  from ..base.ids import base62_12
27
30
  from .run import TracksRun, TracksUpdates
28
- from .sqlrecord import SQLRecord
31
+ from .sqlrecord import Space, SQLRecord
29
32
 
30
33
  if TYPE_CHECKING:
31
34
  from pathlib import Path
@@ -61,22 +64,31 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
61
64
 
62
65
  .. dropdown:: Managing access to storage locations across instances
63
66
 
64
- You can manage access through AWS policies that you attach to your S3 bucket
65
- or leverage LaminHub's fine-grained access management.
67
+ You can manage access through LaminHub's fine-grained access management or
68
+ through AWS policies that you attach to your S3 bucket.
66
69
 
67
- Head over to `https://lamin.ai/{account}/infrastructure`.
68
- By clicking the green button that says "Connect S3 bucket", you enable Lamin to issue federated S3 tokens
69
- for a bucket so that your collaborators can access data based on their permissions in LaminHub.
70
+ To enable access management via LaminHub, head over to `https://lamin.ai/{account}/infrastructure`.
71
+ By clicking the green button that says "Connect S3 bucket", LaminDB will start connecting through federated S3 tokens
72
+ so that your collaborators access data based on their permissions in LaminHub.
70
73
  :doc:`docs:access` has more details.
71
74
 
72
75
  .. image:: https://lamin-site-assets.s3.amazonaws.com/.lamindb/ze8hkgVxVptSSZEU0000.png
73
76
  :width: 800px
74
77
 
78
+ By default, access permissions to a storage location are governed by the access permissions of its managing instance. If you
79
+ want to further restrict access to a storage location, you can move it into a space::
80
+
81
+ space = ln.Space.get(name="my-space")
82
+ storage_loc = ln.Storage.get(root="s3://my-storace-location")
83
+ storage_loc.space = space
84
+ storage_loc.save()
85
+
75
86
  If you don't want to store data in the cloud, you can use local storage locations: :doc:`faq/keep-artifacts-local`.
76
87
 
77
88
  Args:
78
89
  root: `str` The root path of the storage location, e.g., `"./mydir"`, `"s3://my-bucket"`, `"s3://my-bucket/myfolder"`, `"gs://my-bucket/myfolder"`, `"/nfs/shared/datasets/genomics"`, `"/weka/shared/models/"`, ...
79
90
  description: `str | None = None` An optional description.
91
+ space: `Space | None = None` A space to restrict access permissions to the storage location.
80
92
  host: `str | None = None` For local storage locations, pass a globally unique host identifier, e.g. `"my-institute-cluster-1"`, `"my-server-abcd"`, ...
81
93
 
82
94
  See Also:
@@ -107,18 +119,17 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
107
119
 
108
120
  ln.Storage(root="/dir/our-shared-dir", host="our-server-123").save()
109
121
 
110
- Switch to another storage location::
122
+ Globally switch to another storage location::
111
123
 
112
124
  ln.settings.storage = "/dir/our-shared-dir" # or "s3://our-bucket/our-folder", "gs://our-bucket/our-folder", ...
113
125
 
114
- If you're operating in `keep-artifacts-local` mode (:doc:`faq/keep-artifacts-local`), you can switch among additional local storage locations::
126
+ Or if you're operating in `keep-artifacts-local` mode (:doc:`faq/keep-artifacts-local`)::
115
127
 
116
- ln.Storage(root="/dir/our-other-shared-dir", host="our-server-123").save() # create
117
- ln.settings.local_storage = "/dir/our-other-shared-dir" # switch
128
+ ln.settings.local_storage = "/dir/our-other-shared-dir"
118
129
 
119
130
  View all storage locations used in your LaminDB instance::
120
131
 
121
- ln.Storage.df()
132
+ ln.Storage.to_dataframe()
122
133
 
123
134
  Notes:
124
135
 
@@ -146,6 +157,7 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
146
157
 
147
158
  class Meta(SQLRecord.Meta, TracksRun.Meta, TracksUpdates.Meta):
148
159
  abstract = False
160
+ app_label = "lamindb"
149
161
 
150
162
  _name_field: str = "root"
151
163
 
@@ -174,6 +186,7 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
174
186
  root: str,
175
187
  *,
176
188
  description: str | None = None,
189
+ space: Space | None = None,
177
190
  host: str | None = None,
178
191
  ): ...
179
192
 
@@ -190,6 +203,8 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
190
203
  ):
191
204
  if len(args) == len(self._meta.concrete_fields):
192
205
  super().__init__(*args)
206
+ self._old_space = self.space
207
+ self._old_space_id = self.space_id
193
208
  return None
194
209
  if args:
195
210
  assert len(args) == 1, ( # noqa: S101
@@ -213,17 +228,30 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
213
228
  ).one_or_none()
214
229
  else:
215
230
  storage_record = Storage.filter(root=kwargs["root"]).one_or_none()
231
+ space = kwargs.get("space", None)
216
232
  if storage_record is not None:
217
233
  from .sqlrecord import init_self_from_db
218
234
 
219
235
  init_self_from_db(self, storage_record)
236
+ self._old_space = self.space
237
+ self._old_space_id = self.space_id
220
238
  return None
221
239
 
222
240
  skip_preparation = kwargs.pop("_skip_preparation", False)
223
241
  if skip_preparation:
242
+ assert space is None, "`space` must not be set if _skip_preparation is True" # noqa: S101
224
243
  super().__init__(*args, **kwargs)
225
244
  return None
226
245
 
246
+ space_uuid = None
247
+ if space is not None:
248
+ hub_space_record = select_space(space.uid)
249
+ if hub_space_record is None:
250
+ raise ValueError(
251
+ "Please first create a space on the hub: https://docs.lamin.ai/access"
252
+ )
253
+ space_uuid = UUID(hub_space_record["id"])
254
+
227
255
  # instance_id won't take effect if
228
256
  # - there is no write access
229
257
  # - the storage location is already managed by another instance
@@ -232,8 +260,8 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
232
260
  instance_id=setup_settings.instance._id,
233
261
  instance_slug=setup_settings.instance.slug,
234
262
  register_hub=setup_settings.instance.is_on_hub,
235
- prevent_register_hub=not setup_settings.instance.is_on_hub,
236
263
  region=kwargs.get("region", None), # host was renamed to region already
264
+ space_uuid=space_uuid,
237
265
  )
238
266
  # ssettings performed validation and normalization of the root path
239
267
  kwargs["root"] = ssettings.root_as_str # noqa: S101
@@ -274,6 +302,8 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
274
302
  f"{managed_message} storage location at {kwargs['root']}{is_managed_by_instance}{hub_message}"
275
303
  )
276
304
  super().__init__(**kwargs)
305
+ self._old_space = self.space
306
+ self._old_space_id = self.space_id
277
307
 
278
308
  @property
279
309
  def host(self) -> str | None:
@@ -296,10 +326,25 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
296
326
  access_token = self._access_token if hasattr(self, "_access_token") else None
297
327
  return create_path(self.root, access_token=access_token)
298
328
 
299
- def delete(self) -> None:
329
+ def save(self, *args, **kwargs):
330
+ """Save the storage record."""
331
+ if hasattr(self, "_old_space") and hasattr(self, "_old_space_id"):
332
+ if (
333
+ self._old_space != self.space or self._old_space_id != self.space_id
334
+ ): # space_id is automatically handled by field tracker according to Claude
335
+ update_storage_with_space(
336
+ storage_lnid=self.uid, space_lnid=self.space.uid
337
+ )
338
+ super().save(*args, **kwargs)
339
+ return self
340
+
341
+ def delete(self) -> None: # type: ignore
342
+ # type ignore is there because we don't use a trash here unlike everywhere else
300
343
  """Delete the storage location.
301
344
 
302
345
  This errors in case the storage location is not empty.
346
+
347
+ Unlike other `SQLRecord`-based registries, this does *not* move the storage record into the trash.
303
348
  """
304
349
  from .. import settings
305
350
 
@@ -324,4 +369,4 @@ class Storage(SQLRecord, TracksRun, TracksUpdates):
324
369
  ssettings._mark_storage_root.unlink(
325
370
  missing_ok=True # this is totally weird, but needed on Py3.11
326
371
  )
327
- super().delete()
372
+ super(SQLRecord, self).delete()
@@ -30,6 +30,22 @@ if TYPE_CHECKING:
30
30
  from .ulabel import ULabel
31
31
 
32
32
 
33
+ def delete_transform_relations(transform: Transform):
34
+ from .project import TransformProject
35
+
36
+ # query all runs and delete their associated report and env artifacts
37
+ runs = Run.filter(transform=transform)
38
+ for run in runs:
39
+ delete_run_artifacts(run)
40
+ # CASCADE doesn't do the job below because run_id might be protected through run__transform=self
41
+ # hence, proactively delete the label links
42
+ qs = TransformProject.filter(transform=transform)
43
+ if qs.exists():
44
+ qs.delete()
45
+ # at this point, all artifacts have been taken care of
46
+ # and one can now leverage CASCADE delete
47
+
48
+
33
49
  # does not inherit from TracksRun because the Transform
34
50
  # is needed to define a run
35
51
  class Transform(SQLRecord, IsVersioned):
@@ -95,6 +111,7 @@ class Transform(SQLRecord, IsVersioned):
95
111
 
96
112
  class Meta(SQLRecord.Meta, IsVersioned.Meta):
97
113
  abstract = False
114
+ app_label = "lamindb"
98
115
  unique_together = ("key", "hash")
99
116
 
100
117
  _len_stem_uid: int = 12
@@ -315,23 +332,6 @@ class Transform(SQLRecord, IsVersioned):
315
332
  """The latest run of this transform."""
316
333
  return self.runs.order_by("-started_at").first()
317
334
 
318
- def delete(self) -> None:
319
- """Delete."""
320
- from .project import TransformProject
321
-
322
- # query all runs and delete their artifacts
323
- runs = Run.filter(transform=self)
324
- for run in runs:
325
- delete_run_artifacts(run)
326
- # CASCADE doesn't do the job below because run_id might be protected through run__transform=self
327
- # hence, proactively delete the labels
328
- qs = TransformProject.filter(transform=self)
329
- if qs.exists():
330
- qs.delete()
331
- # at this point, all artifacts have been taken care of
332
- # we can now leverage CASCADE delete
333
- super().delete()
334
-
335
335
  def view_lineage(self, with_successors: bool = False, distance: int = 5):
336
336
  """View lineage of transforms.
337
337
 
lamindb/models/ulabel.py CHANGED
@@ -83,11 +83,12 @@ class ULabel(SQLRecord, HasParents, CanCurate, TracksRun, TracksUpdates):
83
83
 
84
84
  Query an artifact by ulabel:
85
85
 
86
- >>> ln.Artifact.filter(ulabels=train_split).df()
86
+ >>> ln.Artifact.filter(ulabels=train_split).to_dataframe()
87
87
  """
88
88
 
89
89
  class Meta(SQLRecord.Meta, TracksRun.Meta, TracksUpdates.Meta):
90
90
  abstract = False
91
+ app_label = "lamindb"
91
92
 
92
93
  _name_field: str = "name"
93
94
 
@@ -221,6 +222,7 @@ class ArtifactULabel(BaseSQLRecord, IsLink, TracksRun):
221
222
  class Meta:
222
223
  # can have the same label linked to the same artifact if the feature is
223
224
  # different
225
+ app_label = "lamindb"
224
226
  unique_together = ("artifact", "ulabel", "feature")
225
227
 
226
228
 
@@ -230,6 +232,7 @@ class TransformULabel(BaseSQLRecord, IsLink, TracksRun):
230
232
  ulabel: ULabel = ForeignKey(ULabel, PROTECT, related_name="links_transform")
231
233
 
232
234
  class Meta:
235
+ app_label = "lamindb"
233
236
  unique_together = ("transform", "ulabel")
234
237
 
235
238
 
@@ -247,6 +250,7 @@ class RunULabel(BaseSQLRecord, IsLink):
247
250
  """Creator of record."""
248
251
 
249
252
  class Meta:
253
+ app_label = "lamindb"
250
254
  unique_together = ("run", "ulabel")
251
255
 
252
256
 
@@ -263,4 +267,5 @@ class CollectionULabel(BaseSQLRecord, IsLink, TracksRun):
263
267
  feature_ref_is_name: bool | None = BooleanField(null=True)
264
268
 
265
269
  class Meta:
270
+ app_label = "lamindb"
266
271
  unique_together = ("collection", "ulabel")
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.3
2
2
  Name: lamindb
3
- Version: 1.10.2
3
+ Version: 1.11a1
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
@@ -9,10 +9,9 @@ Classifier: Programming Language :: Python :: 3.10
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
- License-File: LICENSE
13
12
  Requires-Dist: lamin_utils==0.15.0
14
- Requires-Dist: lamin_cli==1.6.1
15
- Requires-Dist: lamindb_setup[aws]==1.9.1
13
+ Requires-Dist: lamin_cli==1.7.0
14
+ Requires-Dist: lamindb_setup[aws]==1.10.0
16
15
  Requires-Dist: pyyaml
17
16
  Requires-Dist: pyarrow
18
17
  Requires-Dist: pandera>=0.24.0