lamindb 1.4.0__py3-none-any.whl → 1.5.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 (55) hide show
  1. lamindb/__init__.py +52 -36
  2. lamindb/_finish.py +17 -10
  3. lamindb/_tracked.py +1 -1
  4. lamindb/base/__init__.py +3 -1
  5. lamindb/base/fields.py +40 -22
  6. lamindb/base/ids.py +1 -94
  7. lamindb/base/types.py +2 -0
  8. lamindb/base/uids.py +117 -0
  9. lamindb/core/_context.py +177 -89
  10. lamindb/core/_settings.py +38 -25
  11. lamindb/core/datasets/__init__.py +11 -4
  12. lamindb/core/datasets/_core.py +5 -5
  13. lamindb/core/datasets/_small.py +0 -93
  14. lamindb/core/datasets/mini_immuno.py +172 -0
  15. lamindb/core/loaders.py +1 -1
  16. lamindb/core/storage/_backed_access.py +100 -6
  17. lamindb/core/storage/_polars_lazy_df.py +51 -0
  18. lamindb/core/storage/_pyarrow_dataset.py +15 -30
  19. lamindb/core/storage/objects.py +6 -0
  20. lamindb/core/subsettings/__init__.py +2 -0
  21. lamindb/core/subsettings/_annotation_settings.py +11 -0
  22. lamindb/curators/__init__.py +7 -3349
  23. lamindb/curators/_legacy.py +2056 -0
  24. lamindb/curators/core.py +1546 -0
  25. lamindb/errors.py +11 -0
  26. lamindb/examples/__init__.py +27 -0
  27. lamindb/examples/schemas/__init__.py +12 -0
  28. lamindb/examples/schemas/_anndata.py +25 -0
  29. lamindb/examples/schemas/_simple.py +19 -0
  30. lamindb/integrations/_vitessce.py +8 -5
  31. lamindb/migrations/0091_alter_featurevalue_options_alter_space_options_and_more.py +24 -0
  32. lamindb/migrations/0092_alter_artifactfeaturevalue_artifact_and_more.py +75 -0
  33. lamindb/models/__init__.py +4 -1
  34. lamindb/models/_describe.py +21 -4
  35. lamindb/models/_feature_manager.py +365 -286
  36. lamindb/models/_label_manager.py +8 -2
  37. lamindb/models/artifact.py +173 -95
  38. lamindb/models/artifact_set.py +122 -0
  39. lamindb/models/collection.py +73 -52
  40. lamindb/models/core.py +1 -1
  41. lamindb/models/feature.py +51 -17
  42. lamindb/models/has_parents.py +2 -2
  43. lamindb/models/project.py +1 -1
  44. lamindb/models/query_manager.py +221 -22
  45. lamindb/models/query_set.py +245 -171
  46. lamindb/models/record.py +62 -243
  47. lamindb/models/run.py +4 -4
  48. lamindb/models/save.py +8 -2
  49. lamindb/models/schema.py +458 -181
  50. lamindb/models/transform.py +2 -2
  51. lamindb/models/ulabel.py +8 -5
  52. {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/METADATA +6 -6
  53. {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/RECORD +55 -42
  54. {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/LICENSE +0 -0
  55. {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/WHEEL +0 -0
@@ -4,21 +4,22 @@ import re
4
4
  from collections import UserList
5
5
  from collections.abc import Iterable
6
6
  from collections.abc import Iterable as IterableType
7
+ from datetime import datetime, timezone
7
8
  from typing import TYPE_CHECKING, Any, Generic, NamedTuple, TypeVar, Union
8
9
 
9
10
  import pandas as pd
10
11
  from django.core.exceptions import FieldError
11
12
  from django.db import models
12
- from django.db.models import F, ForeignKey, ManyToManyField, Subquery
13
+ from django.db.models import F, ForeignKey, ManyToManyField, Q, Subquery
13
14
  from django.db.models.fields.related import ForeignObjectRel
14
15
  from lamin_utils import logger
15
16
  from lamindb_setup.core._docs import doc_args
16
17
 
17
- from lamindb.models._is_versioned import IsVersioned
18
- from lamindb.models.record import Record
19
-
20
18
  from ..errors import DoesNotExist
21
- from .can_curate import CanCurate
19
+ from ._is_versioned import IsVersioned
20
+ from .can_curate import CanCurate, _inspect, _standardize, _validate
21
+ from .query_manager import _lookup, _search
22
+ from .record import Record
22
23
 
23
24
  if TYPE_CHECKING:
24
25
  from lamindb.base.types import ListLike, StrField
@@ -226,6 +227,11 @@ class RecordList(UserList, Generic[T]):
226
227
  values = [record.__dict__ for record in self.data]
227
228
  return pd.DataFrame(values, columns=keys)
228
229
 
230
+ def list(
231
+ self, field: str
232
+ ) -> list[str]: # meaningful to be parallel with list() in QuerySet
233
+ return [getattr(record, field) for record in self.data]
234
+
229
235
  def one(self) -> T:
230
236
  """Exactly one result. Throws error if there are more or none."""
231
237
  return one_helper(self)
@@ -239,7 +245,9 @@ class RecordList(UserList, Generic[T]):
239
245
 
240
246
 
241
247
  def get_basic_field_names(
242
- qs: QuerySet, include: list[str], features: bool | list[str] = False
248
+ qs: QuerySet,
249
+ include: list[str],
250
+ features_input: bool | list[str],
243
251
  ) -> list[str]:
244
252
  exclude_field_names = ["updated_at"]
245
253
  field_names = [
@@ -271,27 +279,40 @@ def get_basic_field_names(
271
279
  if field_names[0] != "uid" and "uid" in field_names:
272
280
  field_names.remove("uid")
273
281
  field_names.insert(0, "uid")
274
- if include or features:
275
- subset_field_names = field_names[:4]
282
+ if (
283
+ include or features_input
284
+ ): # if there is features_input, reduce fields to just the first 3
285
+ subset_field_names = field_names[:3]
276
286
  intersection = set(field_names) & set(include)
277
287
  subset_field_names += list(intersection)
278
288
  field_names = subset_field_names
279
289
  return field_names
280
290
 
281
291
 
282
- def get_feature_annotate_kwargs(show_features: bool | list[str]) -> dict[str, Any]:
292
+ def get_feature_annotate_kwargs(
293
+ features: bool | list[str] | None,
294
+ ) -> tuple[dict[str, Any], list[str], QuerySet]:
283
295
  from lamindb.models import (
284
296
  Artifact,
285
297
  Feature,
286
298
  )
287
299
 
288
- features = Feature.filter()
289
- if isinstance(show_features, list):
290
- features.filter(name__in=show_features)
300
+ feature_qs = Feature.filter()
301
+ if isinstance(features, list):
302
+ feature_qs = feature_qs.filter(name__in=features)
303
+ feature_names = features
304
+ else: # features is True -- only consider categorical features from ULabel and non-categorical features
305
+ feature_qs = feature_qs.filter(
306
+ Q(~Q(dtype__startswith="cat[")) | Q(dtype__startswith="cat[ULabel")
307
+ )
308
+ feature_names = feature_qs.list("name")
309
+ logger.important(
310
+ f"queried for all categorical features with dtype 'cat[ULabel...'] and non-categorical features: ({len(feature_names)}) {feature_names}"
311
+ )
291
312
  # Get the categorical features
292
313
  cat_feature_types = {
293
314
  feature.dtype.replace("cat[", "").replace("]", "")
294
- for feature in features
315
+ for feature in feature_qs
295
316
  if feature.dtype.startswith("cat[")
296
317
  }
297
318
  # Get relationships of labels and features
@@ -327,7 +348,7 @@ def get_feature_annotate_kwargs(show_features: bool | list[str]) -> dict[str, An
327
348
  "_feature_values__feature__name"
328
349
  )
329
350
  annotate_kwargs["_feature_values__value"] = F("_feature_values__value")
330
- return annotate_kwargs
351
+ return annotate_kwargs, feature_names, feature_qs
331
352
 
332
353
 
333
354
  # https://claude.ai/share/16280046-6ae5-4f6a-99ac-dec01813dc3c
@@ -381,45 +402,67 @@ def analyze_lookup_cardinality(
381
402
  return result
382
403
 
383
404
 
405
+ def reorder_subset_columns_in_df(df: pd.DataFrame, column_order: list[str], position=3):
406
+ valid_columns = [col for col in column_order if col in df.columns]
407
+ all_cols = df.columns.tolist()
408
+ remaining_cols = [col for col in all_cols if col not in valid_columns]
409
+ new_order = remaining_cols[:position] + valid_columns + remaining_cols[position:]
410
+ return df[new_order]
411
+
412
+
384
413
  # https://lamin.ai/laminlabs/lamindata/transform/BblTiuKxsb2g0003
385
414
  # https://claude.ai/chat/6ea2498c-944d-4e7a-af08-29e5ddf637d2
386
415
  def reshape_annotate_result(
387
- field_names: list[str],
388
416
  df: pd.DataFrame,
389
- extra_columns: dict[str, str] | None = None,
390
- features: bool | list[str] = False,
417
+ field_names: list[str],
418
+ cols_from_include: dict[str, str] | None,
419
+ feature_names: list[str],
420
+ feature_qs: QuerySet | None,
391
421
  ) -> pd.DataFrame:
392
- """Reshapes experimental data with optional feature handling.
422
+ """Reshapes tidy table to wide format.
393
423
 
394
- Parameters:
395
- field_names: List of basic fields to include in result
396
- df: Input dataframe with experimental data
397
- extra_columns: Dict specifying additional columns to process with types ('one' or 'many')
398
- e.g., {'ulabels__name': 'many', 'created_by__name': 'one'}
399
- features: If False, skip feature processing. If True, process all features.
400
- If list of strings, only process specified features.
401
-
402
- Returns:
403
- DataFrame with reshaped data
424
+ Args:
425
+ field_names: List of basic fields to include in result
426
+ df: Input dataframe with experimental data
427
+ extra_columns: Dict specifying additional columns to process with types ('one' or 'many')
428
+ e.g., {'ulabels__name': 'many', 'created_by__name': 'one'}
429
+ feature_names: Feature names.
404
430
  """
405
- extra_columns = extra_columns or {}
406
-
407
- # Initialize result with basic fields
408
- result = df[field_names].drop_duplicates(subset=["id"])
431
+ cols_from_include = cols_from_include or {}
409
432
 
410
- # Process features if requested
411
- if features:
412
- # Handle _feature_values if columns exist
433
+ # initialize result with basic fields
434
+ result = df[field_names]
435
+ # process features if requested
436
+ if feature_names:
437
+ # handle feature_values
413
438
  feature_cols = ["_feature_values__feature__name", "_feature_values__value"]
414
439
  if all(col in df.columns for col in feature_cols):
415
- feature_values = process_feature_values(df, features)
440
+ # Create two separate dataframes - one for dict values and one for non-dict values
441
+ is_dict = df["_feature_values__value"].apply(lambda x: isinstance(x, dict))
442
+ dict_df, non_dict_df = df[is_dict], df[~is_dict]
443
+
444
+ # Process non-dict values using set aggregation
445
+ non_dict_features = non_dict_df.groupby(
446
+ ["id", "_feature_values__feature__name"]
447
+ )["_feature_values__value"].agg(set)
448
+
449
+ # Process dict values using first aggregation
450
+ dict_features = dict_df.groupby(["id", "_feature_values__feature__name"])[
451
+ "_feature_values__value"
452
+ ].agg("first")
453
+
454
+ # Combine the results
455
+ combined_features = pd.concat([non_dict_features, dict_features])
456
+
457
+ # Unstack and reset index
458
+ feature_values = combined_features.unstack().reset_index()
416
459
  if not feature_values.empty:
417
- for col in feature_values.columns:
418
- if col in result.columns:
419
- continue
420
- result.insert(4, col, feature_values[col])
460
+ result = result.join(
461
+ feature_values.set_index("id"),
462
+ on="id",
463
+ )
421
464
 
422
- # Handle links features if they exist
465
+ # handle categorical features
423
466
  links_features = [
424
467
  col
425
468
  for col in df.columns
@@ -427,32 +470,34 @@ def reshape_annotate_result(
427
470
  ]
428
471
 
429
472
  if links_features:
430
- result = process_links_features(df, result, links_features, features)
431
-
432
- # Process extra columns
433
- if extra_columns:
434
- result = process_extra_columns(df, result, extra_columns)
435
-
436
- return result
437
-
473
+ result = process_links_features(df, result, links_features, feature_names)
474
+
475
+ def extract_single_element(s):
476
+ if not hasattr(s, "__len__"): # is NaN or other scalar
477
+ return s
478
+ if len(s) != 1:
479
+ # TODO: below should depend on feature._expect_many
480
+ # logger.warning(
481
+ # f"expected single value because `feature._expect_many is False` but got set {len(s)} elements: {s}"
482
+ # )
483
+ return s
484
+ return next(iter(s))
485
+
486
+ for feature in feature_qs:
487
+ if feature.name in result.columns:
488
+ # TODO: make dependent on feature._expect_many through
489
+ # lambda x: extract_single_element(x, feature)
490
+ result[feature.name] = result[feature.name].apply(
491
+ extract_single_element
492
+ )
438
493
 
439
- def process_feature_values(
440
- df: pd.DataFrame, features: bool | list[str]
441
- ) -> pd.DataFrame:
442
- """Process _feature_values columns."""
443
- feature_values = df.groupby(["id", "_feature_values__feature__name"])[
444
- "_feature_values__value"
445
- ].agg(set)
494
+ # sort columns
495
+ result = reorder_subset_columns_in_df(result, feature_names)
446
496
 
447
- # Filter features if specific ones requested
448
- if isinstance(features, list):
449
- feature_values = feature_values[
450
- feature_values.index.get_level_values(
451
- "_feature_values__feature__name"
452
- ).isin(features)
453
- ]
497
+ if cols_from_include:
498
+ result = process_cols_from_include(df, result, cols_from_include)
454
499
 
455
- return feature_values.unstack().reset_index()
500
+ return result.drop_duplicates(subset=["id"])
456
501
 
457
502
 
458
503
  def process_links_features(
@@ -488,12 +533,12 @@ def process_links_features(
488
533
  for feature_name in feature_names:
489
534
  mask = df[feature_col] == feature_name
490
535
  feature_values = df[mask].groupby("id")[value_col].agg(set)
491
- result.insert(4, feature_name, result["id"].map(feature_values))
536
+ result.insert(3, feature_name, result["id"].map(feature_values))
492
537
 
493
538
  return result
494
539
 
495
540
 
496
- def process_extra_columns(
541
+ def process_cols_from_include(
497
542
  df: pd.DataFrame, result: pd.DataFrame, extra_columns: dict[str, str]
498
543
  ) -> pd.DataFrame:
499
544
  """Process additional columns based on their specified types."""
@@ -504,58 +549,87 @@ def process_extra_columns(
504
549
  continue
505
550
 
506
551
  values = df.groupby("id")[col].agg(set if col_type == "many" else "first")
507
- result.insert(4, col, result["id"].map(values))
552
+ result.insert(3, col, result["id"].map(values))
508
553
 
509
554
  return result
510
555
 
511
556
 
512
- class QuerySet(models.QuerySet):
557
+ class BasicQuerySet(models.QuerySet):
513
558
  """Sets of records returned by queries.
514
559
 
515
560
  See Also:
516
561
 
517
- `django QuerySet <https://docs.djangoproject.com/en/4.2/ref/models/querysets/>`__
562
+ `django QuerySet <https://docs.djangoproject.com/en/stable/ref/models/querysets/>`__
518
563
 
519
564
  Examples:
520
565
 
521
- >>> ULabel(name="my label").save()
522
- >>> queryset = ULabel.filter(name="my label")
523
- >>> queryset
566
+ Any filter statement produces a query set::
567
+
568
+ queryset = Registry.filter(name__startswith="keyword")
524
569
  """
525
570
 
571
+ def __new__(cls, model=None, query=None, using=None, hints=None):
572
+ from lamindb.models import Artifact, ArtifactSet
573
+
574
+ # If the model is Artifact, create a new class
575
+ # for BasicQuerySet or QuerySet that inherits from ArtifactSet.
576
+ # This allows to add artifact specific functionality to all classes
577
+ # inheriting from BasicQuerySet.
578
+ # Thus all query sets of artifacts (and only of artifacts)
579
+ # will have functions from ArtifactSet.
580
+ if model is Artifact and not issubclass(cls, ArtifactSet):
581
+ new_cls = type("Artifact" + cls.__name__, (cls, ArtifactSet), {})
582
+ else:
583
+ new_cls = cls
584
+ return object.__new__(new_cls)
585
+
526
586
  @doc_args(Record.df.__doc__)
527
587
  def df(
528
588
  self,
529
589
  include: str | list[str] | None = None,
530
- features: bool | list[str] = False,
590
+ features: bool | list[str] | None = None,
531
591
  ) -> pd.DataFrame:
532
592
  """{}""" # noqa: D415
593
+ time = datetime.now(timezone.utc)
533
594
  if include is None:
534
- include = []
595
+ include_input = []
535
596
  elif isinstance(include, str):
536
- include = [include]
537
- include = get_backward_compat_filter_kwargs(self, include)
538
- field_names = get_basic_field_names(self, include, features) # type: ignore
597
+ include_input = [include]
598
+ else:
599
+ include_input = include
600
+ features_input = [] if features is None else features
601
+ include = get_backward_compat_filter_kwargs(self, include_input)
602
+ field_names = get_basic_field_names(self, include_input, features_input)
539
603
 
540
604
  annotate_kwargs = {}
605
+ feature_names: list[str] = []
606
+ feature_qs = None
541
607
  if features:
542
- annotate_kwargs.update(get_feature_annotate_kwargs(features))
543
- if include:
544
- include = include.copy()[::-1] # type: ignore
545
- include_kwargs = {s: F(s) for s in include if s not in field_names}
608
+ feature_annotate_kwargs, feature_names, feature_qs = (
609
+ get_feature_annotate_kwargs(features)
610
+ )
611
+ time = logger.debug("finished feature_annotate_kwargs", time=time)
612
+ annotate_kwargs.update(feature_annotate_kwargs)
613
+ if include_input:
614
+ include_input = include_input.copy()[::-1] # type: ignore
615
+ include_kwargs = {s: F(s) for s in include_input if s not in field_names}
546
616
  annotate_kwargs.update(include_kwargs)
547
617
  if annotate_kwargs:
548
618
  id_subquery = self.values("id")
619
+ time = logger.debug("finished get id values", time=time)
549
620
  # for annotate, we want the queryset without filters so that joins don't affect the annotations
550
621
  query_set_without_filters = self.model.objects.filter(
551
622
  id__in=Subquery(id_subquery)
552
623
  )
624
+ time = logger.debug("finished get query_set_without_filters", time=time)
553
625
  if self.query.order_by:
554
626
  # Apply the same ordering to the new queryset
555
627
  query_set_without_filters = query_set_without_filters.order_by(
556
628
  *self.query.order_by
557
629
  )
630
+ time = logger.debug("finished order by", time=time)
558
631
  queryset = query_set_without_filters.annotate(**annotate_kwargs)
632
+ time = logger.debug("finished annotate", time=time)
559
633
  else:
560
634
  queryset = self
561
635
 
@@ -563,12 +637,18 @@ class QuerySet(models.QuerySet):
563
637
  if len(df) == 0:
564
638
  df = pd.DataFrame({}, columns=field_names)
565
639
  return df
566
- extra_cols = analyze_lookup_cardinality(self.model, include) # type: ignore
567
- df_reshaped = reshape_annotate_result(field_names, df, extra_cols, features)
640
+ time = logger.debug("finished creating first dataframe", time=time)
641
+ cols_from_include = analyze_lookup_cardinality(self.model, include_input) # type: ignore
642
+ time = logger.debug("finished analyze_lookup_cardinality", time=time)
643
+ df_reshaped = reshape_annotate_result(
644
+ df, field_names, cols_from_include, feature_names, feature_qs
645
+ )
646
+ time = logger.debug("finished reshape_annotate_result", time=time)
568
647
  pk_name = self.model._meta.pk.name
569
648
  pk_column_name = pk_name if pk_name in df.columns else f"{pk_name}_id"
570
649
  if pk_column_name in df_reshaped.columns:
571
650
  df_reshaped = df_reshaped.set_index(pk_column_name)
651
+ time = logger.debug("finished", time=time)
572
652
  return df_reshaped
573
653
 
574
654
  def delete(self, *args, **kwargs):
@@ -581,10 +661,12 @@ class QuerySet(models.QuerySet):
581
661
  logger.important(f"deleting {record}")
582
662
  record.delete(*args, **kwargs)
583
663
  else:
584
- self._delete_base_class(*args, **kwargs)
664
+ super().delete(*args, **kwargs)
585
665
 
586
- def list(self, field: str | None = None) -> list[Record]:
587
- """Populate a list with the results.
666
+ def list(self, field: str | None = None) -> list[Record] | list[str]:
667
+ """Populate an (unordered) list with the results.
668
+
669
+ Note that the order in this list is only meaningful if you ordered the underlying query set with `.order_by()`.
588
670
 
589
671
  Examples:
590
672
  >>> queryset.list() # list of records
@@ -593,6 +675,7 @@ class QuerySet(models.QuerySet):
593
675
  if field is None:
594
676
  return list(self)
595
677
  else:
678
+ # list casting is necessary because values_list does not return a list
596
679
  return list(self.values_list(field, flat=True))
597
680
 
598
681
  def first(self) -> Record | None:
@@ -605,6 +688,82 @@ class QuerySet(models.QuerySet):
605
688
  return None
606
689
  return self[0]
607
690
 
691
+ def one(self) -> Record:
692
+ """Exactly one result. Raises error if there are more or none."""
693
+ return one_helper(self)
694
+
695
+ def one_or_none(self) -> Record | None:
696
+ """At most one result. Returns it if there is one, otherwise returns ``None``.
697
+
698
+ Examples:
699
+ >>> ULabel.filter(name="benchmark").one_or_none()
700
+ >>> ULabel.filter(name="non existing label").one_or_none()
701
+ """
702
+ if len(self) == 0:
703
+ return None
704
+ elif len(self) == 1:
705
+ return self[0]
706
+ else:
707
+ raise MultipleResultsFound(self.all())
708
+
709
+ def latest_version(self) -> QuerySet:
710
+ """Filter every version family by latest version."""
711
+ if issubclass(self.model, IsVersioned):
712
+ return self.filter(is_latest=True)
713
+ else:
714
+ raise ValueError("Record isn't subclass of `lamindb.core.IsVersioned`")
715
+
716
+ @doc_args(_search.__doc__)
717
+ def search(self, string: str, **kwargs):
718
+ """{}""" # noqa: D415
719
+ return _search(cls=self, string=string, **kwargs)
720
+
721
+ @doc_args(_lookup.__doc__)
722
+ def lookup(self, field: StrField | None = None, **kwargs) -> NamedTuple:
723
+ """{}""" # noqa: D415
724
+ return _lookup(cls=self, field=field, **kwargs)
725
+
726
+ # -------------------------------------------------------------------------------------
727
+ # CanCurate
728
+ # -------------------------------------------------------------------------------------
729
+
730
+ @doc_args(CanCurate.validate.__doc__)
731
+ def validate(self, values: ListLike, field: str | StrField | None = None, **kwargs):
732
+ """{}""" # noqa: D415
733
+ return _validate(cls=self, values=values, field=field, **kwargs)
734
+
735
+ @doc_args(CanCurate.inspect.__doc__)
736
+ def inspect(self, values: ListLike, field: str | StrField | None = None, **kwargs):
737
+ """{}""" # noqa: D415
738
+ return _inspect(cls=self, values=values, field=field, **kwargs)
739
+
740
+ @doc_args(CanCurate.standardize.__doc__)
741
+ def standardize(
742
+ self, values: Iterable, field: str | StrField | None = None, **kwargs
743
+ ):
744
+ """{}""" # noqa: D415
745
+ return _standardize(cls=self, values=values, field=field, **kwargs)
746
+
747
+
748
+ # this differs from BasicQuerySet only in .filter and .get
749
+ # QueryManager returns BasicQuerySet because it is problematic to redefine .filter and .get
750
+ # for a query set used by the default manager
751
+ class QuerySet(BasicQuerySet):
752
+ """Sets of records returned by queries.
753
+
754
+ Implements additional filtering capabilities.
755
+
756
+ See Also:
757
+
758
+ `django QuerySet <https://docs.djangoproject.com/en/4.2/ref/models/querysets/>`__
759
+
760
+ Examples:
761
+
762
+ >>> ULabel(name="my label").save()
763
+ >>> queryset = ULabel.filter(name="my label")
764
+ >>> queryset # an instance of QuerySet
765
+ """
766
+
608
767
  def _handle_unknown_field(self, error: FieldError) -> None:
609
768
  """Suggest available fields if an unknown field was passed."""
610
769
  if "Cannot resolve keyword" in str(error):
@@ -657,88 +816,3 @@ class QuerySet(models.QuerySet):
657
816
  except FieldError as e:
658
817
  self._handle_unknown_field(e)
659
818
  return self
660
-
661
- def one(self) -> Record:
662
- """Exactly one result. Raises error if there are more or none."""
663
- return one_helper(self)
664
-
665
- def one_or_none(self) -> Record | None:
666
- """At most one result. Returns it if there is one, otherwise returns ``None``.
667
-
668
- Examples:
669
- >>> ULabel.filter(name="benchmark").one_or_none()
670
- >>> ULabel.filter(name="non existing label").one_or_none()
671
- """
672
- if len(self) == 0:
673
- return None
674
- elif len(self) == 1:
675
- return self[0]
676
- else:
677
- raise MultipleResultsFound(self.all())
678
-
679
- def latest_version(self) -> QuerySet:
680
- """Filter every version family by latest version."""
681
- if issubclass(self.model, IsVersioned):
682
- return self.filter(is_latest=True)
683
- else:
684
- raise ValueError("Record isn't subclass of `lamindb.core.IsVersioned`")
685
-
686
-
687
- # -------------------------------------------------------------------------------------
688
- # CanCurate
689
- # -------------------------------------------------------------------------------------
690
-
691
-
692
- @doc_args(Record.search.__doc__)
693
- def search(self, string: str, **kwargs):
694
- """{}""" # noqa: D415
695
- from .record import _search
696
-
697
- return _search(cls=self, string=string, **kwargs)
698
-
699
-
700
- @doc_args(Record.lookup.__doc__)
701
- def lookup(self, field: StrField | None = None, **kwargs) -> NamedTuple:
702
- """{}""" # noqa: D415
703
- from .record import _lookup
704
-
705
- return _lookup(cls=self, field=field, **kwargs)
706
-
707
-
708
- @doc_args(CanCurate.validate.__doc__)
709
- def validate(self, values: ListLike, field: str | StrField | None = None, **kwargs):
710
- """{}""" # noqa: D415
711
- from .can_curate import _validate
712
-
713
- return _validate(cls=self, values=values, field=field, **kwargs)
714
-
715
-
716
- @doc_args(CanCurate.inspect.__doc__)
717
- def inspect(self, values: ListLike, field: str | StrField | None = None, **kwargs):
718
- """{}""" # noqa: D415
719
- from .can_curate import _inspect
720
-
721
- return _inspect(cls=self, values=values, field=field, **kwargs)
722
-
723
-
724
- @doc_args(CanCurate.standardize.__doc__)
725
- def standardize(self, values: Iterable, field: str | StrField | None = None, **kwargs):
726
- """{}""" # noqa: D415
727
- from .can_curate import _standardize
728
-
729
- return _standardize(cls=self, values=values, field=field, **kwargs)
730
-
731
-
732
- models.QuerySet.df = QuerySet.df
733
- models.QuerySet.list = QuerySet.list
734
- models.QuerySet.first = QuerySet.first
735
- models.QuerySet.one = QuerySet.one
736
- models.QuerySet.one_or_none = QuerySet.one_or_none
737
- models.QuerySet.latest_version = QuerySet.latest_version
738
- models.QuerySet.search = search
739
- models.QuerySet.lookup = lookup
740
- models.QuerySet.validate = validate
741
- models.QuerySet.inspect = inspect
742
- models.QuerySet.standardize = standardize
743
- models.QuerySet._delete_base_class = models.QuerySet.delete
744
- models.QuerySet.delete = QuerySet.delete