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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) 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 +203 -102
  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/_tiledbsoma.py +29 -13
  20. lamindb/core/storage/objects.py +6 -0
  21. lamindb/core/subsettings/__init__.py +2 -0
  22. lamindb/core/subsettings/_annotation_settings.py +11 -0
  23. lamindb/curators/__init__.py +7 -3349
  24. lamindb/curators/_legacy.py +2056 -0
  25. lamindb/curators/core.py +1534 -0
  26. lamindb/errors.py +11 -0
  27. lamindb/examples/__init__.py +27 -0
  28. lamindb/examples/schemas/__init__.py +12 -0
  29. lamindb/examples/schemas/_anndata.py +25 -0
  30. lamindb/examples/schemas/_simple.py +19 -0
  31. lamindb/integrations/_vitessce.py +8 -5
  32. lamindb/migrations/0091_alter_featurevalue_options_alter_space_options_and_more.py +24 -0
  33. lamindb/migrations/0092_alter_artifactfeaturevalue_artifact_and_more.py +75 -0
  34. lamindb/migrations/0093_alter_schemacomponent_unique_together.py +16 -0
  35. lamindb/models/__init__.py +4 -1
  36. lamindb/models/_describe.py +21 -4
  37. lamindb/models/_feature_manager.py +382 -287
  38. lamindb/models/_label_manager.py +8 -2
  39. lamindb/models/artifact.py +177 -106
  40. lamindb/models/artifact_set.py +122 -0
  41. lamindb/models/collection.py +73 -52
  42. lamindb/models/core.py +1 -1
  43. lamindb/models/feature.py +51 -17
  44. lamindb/models/has_parents.py +69 -14
  45. lamindb/models/project.py +1 -1
  46. lamindb/models/query_manager.py +221 -22
  47. lamindb/models/query_set.py +247 -172
  48. lamindb/models/record.py +65 -247
  49. lamindb/models/run.py +4 -4
  50. lamindb/models/save.py +8 -2
  51. lamindb/models/schema.py +456 -184
  52. lamindb/models/transform.py +2 -2
  53. lamindb/models/ulabel.py +8 -5
  54. {lamindb-1.4.0.dist-info → lamindb-1.5.1.dist-info}/METADATA +6 -6
  55. {lamindb-1.4.0.dist-info → lamindb-1.5.1.dist-info}/RECORD +57 -43
  56. {lamindb-1.4.0.dist-info → lamindb-1.5.1.dist-info}/LICENSE +0 -0
  57. {lamindb-1.4.0.dist-info → lamindb-1.5.1.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,68 @@ 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"])
409
-
410
- # Process features if requested
411
- if features:
412
- # Handle _feature_values if columns exist
431
+ cols_from_include = cols_from_include or {}
432
+
433
+ # initialize result with basic fields, need a copy as we're modifying it
434
+ # will give us warnings otherwise
435
+ result = df[field_names].copy()
436
+ # process features if requested
437
+ if feature_names:
438
+ # handle feature_values
413
439
  feature_cols = ["_feature_values__feature__name", "_feature_values__value"]
414
440
  if all(col in df.columns for col in feature_cols):
415
- feature_values = process_feature_values(df, features)
441
+ # Create two separate dataframes - one for dict values and one for non-dict values
442
+ is_dict = df["_feature_values__value"].apply(lambda x: isinstance(x, dict))
443
+ dict_df, non_dict_df = df[is_dict], df[~is_dict]
444
+
445
+ # Process non-dict values using set aggregation
446
+ non_dict_features = non_dict_df.groupby(
447
+ ["id", "_feature_values__feature__name"]
448
+ )["_feature_values__value"].agg(set)
449
+
450
+ # Process dict values using first aggregation
451
+ dict_features = dict_df.groupby(["id", "_feature_values__feature__name"])[
452
+ "_feature_values__value"
453
+ ].agg("first")
454
+
455
+ # Combine the results
456
+ combined_features = pd.concat([non_dict_features, dict_features])
457
+
458
+ # Unstack and reset index
459
+ feature_values = combined_features.unstack().reset_index()
416
460
  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])
461
+ result = result.join(
462
+ feature_values.set_index("id"),
463
+ on="id",
464
+ )
421
465
 
422
- # Handle links features if they exist
466
+ # handle categorical features
423
467
  links_features = [
424
468
  col
425
469
  for col in df.columns
@@ -427,32 +471,34 @@ def reshape_annotate_result(
427
471
  ]
428
472
 
429
473
  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
-
474
+ result = process_links_features(df, result, links_features, feature_names)
475
+
476
+ def extract_single_element(s):
477
+ if not hasattr(s, "__len__"): # is NaN or other scalar
478
+ return s
479
+ if len(s) != 1:
480
+ # TODO: below should depend on feature._expect_many
481
+ # logger.warning(
482
+ # f"expected single value because `feature._expect_many is False` but got set {len(s)} elements: {s}"
483
+ # )
484
+ return s
485
+ return next(iter(s))
486
+
487
+ for feature in feature_qs:
488
+ if feature.name in result.columns:
489
+ # TODO: make dependent on feature._expect_many through
490
+ # lambda x: extract_single_element(x, feature)
491
+ result[feature.name] = result[feature.name].apply(
492
+ extract_single_element
493
+ )
438
494
 
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)
495
+ # sort columns
496
+ result = reorder_subset_columns_in_df(result, feature_names)
446
497
 
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
- ]
498
+ if cols_from_include:
499
+ result = process_cols_from_include(df, result, cols_from_include)
454
500
 
455
- return feature_values.unstack().reset_index()
501
+ return result.drop_duplicates(subset=["id"])
456
502
 
457
503
 
458
504
  def process_links_features(
@@ -488,12 +534,12 @@ def process_links_features(
488
534
  for feature_name in feature_names:
489
535
  mask = df[feature_col] == feature_name
490
536
  feature_values = df[mask].groupby("id")[value_col].agg(set)
491
- result.insert(4, feature_name, result["id"].map(feature_values))
537
+ result.insert(3, feature_name, result["id"].map(feature_values))
492
538
 
493
539
  return result
494
540
 
495
541
 
496
- def process_extra_columns(
542
+ def process_cols_from_include(
497
543
  df: pd.DataFrame, result: pd.DataFrame, extra_columns: dict[str, str]
498
544
  ) -> pd.DataFrame:
499
545
  """Process additional columns based on their specified types."""
@@ -504,58 +550,87 @@ def process_extra_columns(
504
550
  continue
505
551
 
506
552
  values = df.groupby("id")[col].agg(set if col_type == "many" else "first")
507
- result.insert(4, col, result["id"].map(values))
553
+ result.insert(3, col, result["id"].map(values))
508
554
 
509
555
  return result
510
556
 
511
557
 
512
- class QuerySet(models.QuerySet):
558
+ class BasicQuerySet(models.QuerySet):
513
559
  """Sets of records returned by queries.
514
560
 
515
561
  See Also:
516
562
 
517
- `django QuerySet <https://docs.djangoproject.com/en/4.2/ref/models/querysets/>`__
563
+ `django QuerySet <https://docs.djangoproject.com/en/stable/ref/models/querysets/>`__
518
564
 
519
565
  Examples:
520
566
 
521
- >>> ULabel(name="my label").save()
522
- >>> queryset = ULabel.filter(name="my label")
523
- >>> queryset
567
+ Any filter statement produces a query set::
568
+
569
+ queryset = Registry.filter(name__startswith="keyword")
524
570
  """
525
571
 
572
+ def __new__(cls, model=None, query=None, using=None, hints=None):
573
+ from lamindb.models import Artifact, ArtifactSet
574
+
575
+ # If the model is Artifact, create a new class
576
+ # for BasicQuerySet or QuerySet that inherits from ArtifactSet.
577
+ # This allows to add artifact specific functionality to all classes
578
+ # inheriting from BasicQuerySet.
579
+ # Thus all query sets of artifacts (and only of artifacts)
580
+ # will have functions from ArtifactSet.
581
+ if model is Artifact and not issubclass(cls, ArtifactSet):
582
+ new_cls = type("Artifact" + cls.__name__, (cls, ArtifactSet), {})
583
+ else:
584
+ new_cls = cls
585
+ return object.__new__(new_cls)
586
+
526
587
  @doc_args(Record.df.__doc__)
527
588
  def df(
528
589
  self,
529
590
  include: str | list[str] | None = None,
530
- features: bool | list[str] = False,
591
+ features: bool | list[str] | None = None,
531
592
  ) -> pd.DataFrame:
532
593
  """{}""" # noqa: D415
594
+ time = datetime.now(timezone.utc)
533
595
  if include is None:
534
- include = []
596
+ include_input = []
535
597
  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
598
+ include_input = [include]
599
+ else:
600
+ include_input = include
601
+ features_input = [] if features is None else features
602
+ include = get_backward_compat_filter_kwargs(self, include_input)
603
+ field_names = get_basic_field_names(self, include_input, features_input)
539
604
 
540
605
  annotate_kwargs = {}
606
+ feature_names: list[str] = []
607
+ feature_qs = None
541
608
  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}
609
+ feature_annotate_kwargs, feature_names, feature_qs = (
610
+ get_feature_annotate_kwargs(features)
611
+ )
612
+ time = logger.debug("finished feature_annotate_kwargs", time=time)
613
+ annotate_kwargs.update(feature_annotate_kwargs)
614
+ if include_input:
615
+ include_input = include_input.copy()[::-1] # type: ignore
616
+ include_kwargs = {s: F(s) for s in include_input if s not in field_names}
546
617
  annotate_kwargs.update(include_kwargs)
547
618
  if annotate_kwargs:
548
619
  id_subquery = self.values("id")
620
+ time = logger.debug("finished get id values", time=time)
549
621
  # for annotate, we want the queryset without filters so that joins don't affect the annotations
550
622
  query_set_without_filters = self.model.objects.filter(
551
623
  id__in=Subquery(id_subquery)
552
624
  )
625
+ time = logger.debug("finished get query_set_without_filters", time=time)
553
626
  if self.query.order_by:
554
627
  # Apply the same ordering to the new queryset
555
628
  query_set_without_filters = query_set_without_filters.order_by(
556
629
  *self.query.order_by
557
630
  )
631
+ time = logger.debug("finished order by", time=time)
558
632
  queryset = query_set_without_filters.annotate(**annotate_kwargs)
633
+ time = logger.debug("finished annotate", time=time)
559
634
  else:
560
635
  queryset = self
561
636
 
@@ -563,12 +638,18 @@ class QuerySet(models.QuerySet):
563
638
  if len(df) == 0:
564
639
  df = pd.DataFrame({}, columns=field_names)
565
640
  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)
641
+ time = logger.debug("finished creating first dataframe", time=time)
642
+ cols_from_include = analyze_lookup_cardinality(self.model, include_input) # type: ignore
643
+ time = logger.debug("finished analyze_lookup_cardinality", time=time)
644
+ df_reshaped = reshape_annotate_result(
645
+ df, field_names, cols_from_include, feature_names, feature_qs
646
+ )
647
+ time = logger.debug("finished reshape_annotate_result", time=time)
568
648
  pk_name = self.model._meta.pk.name
569
649
  pk_column_name = pk_name if pk_name in df.columns else f"{pk_name}_id"
570
650
  if pk_column_name in df_reshaped.columns:
571
651
  df_reshaped = df_reshaped.set_index(pk_column_name)
652
+ time = logger.debug("finished", time=time)
572
653
  return df_reshaped
573
654
 
574
655
  def delete(self, *args, **kwargs):
@@ -581,10 +662,12 @@ class QuerySet(models.QuerySet):
581
662
  logger.important(f"deleting {record}")
582
663
  record.delete(*args, **kwargs)
583
664
  else:
584
- self._delete_base_class(*args, **kwargs)
665
+ super().delete(*args, **kwargs)
585
666
 
586
- def list(self, field: str | None = None) -> list[Record]:
587
- """Populate a list with the results.
667
+ def list(self, field: str | None = None) -> list[Record] | list[str]:
668
+ """Populate an (unordered) list with the results.
669
+
670
+ Note that the order in this list is only meaningful if you ordered the underlying query set with `.order_by()`.
588
671
 
589
672
  Examples:
590
673
  >>> queryset.list() # list of records
@@ -593,6 +676,7 @@ class QuerySet(models.QuerySet):
593
676
  if field is None:
594
677
  return list(self)
595
678
  else:
679
+ # list casting is necessary because values_list does not return a list
596
680
  return list(self.values_list(field, flat=True))
597
681
 
598
682
  def first(self) -> Record | None:
@@ -605,6 +689,82 @@ class QuerySet(models.QuerySet):
605
689
  return None
606
690
  return self[0]
607
691
 
692
+ def one(self) -> Record:
693
+ """Exactly one result. Raises error if there are more or none."""
694
+ return one_helper(self)
695
+
696
+ def one_or_none(self) -> Record | None:
697
+ """At most one result. Returns it if there is one, otherwise returns ``None``.
698
+
699
+ Examples:
700
+ >>> ULabel.filter(name="benchmark").one_or_none()
701
+ >>> ULabel.filter(name="non existing label").one_or_none()
702
+ """
703
+ if len(self) == 0:
704
+ return None
705
+ elif len(self) == 1:
706
+ return self[0]
707
+ else:
708
+ raise MultipleResultsFound(self.all())
709
+
710
+ def latest_version(self) -> QuerySet:
711
+ """Filter every version family by latest version."""
712
+ if issubclass(self.model, IsVersioned):
713
+ return self.filter(is_latest=True)
714
+ else:
715
+ raise ValueError("Record isn't subclass of `lamindb.core.IsVersioned`")
716
+
717
+ @doc_args(_search.__doc__)
718
+ def search(self, string: str, **kwargs):
719
+ """{}""" # noqa: D415
720
+ return _search(cls=self, string=string, **kwargs)
721
+
722
+ @doc_args(_lookup.__doc__)
723
+ def lookup(self, field: StrField | None = None, **kwargs) -> NamedTuple:
724
+ """{}""" # noqa: D415
725
+ return _lookup(cls=self, field=field, **kwargs)
726
+
727
+ # -------------------------------------------------------------------------------------
728
+ # CanCurate
729
+ # -------------------------------------------------------------------------------------
730
+
731
+ @doc_args(CanCurate.validate.__doc__)
732
+ def validate(self, values: ListLike, field: str | StrField | None = None, **kwargs):
733
+ """{}""" # noqa: D415
734
+ return _validate(cls=self, values=values, field=field, **kwargs)
735
+
736
+ @doc_args(CanCurate.inspect.__doc__)
737
+ def inspect(self, values: ListLike, field: str | StrField | None = None, **kwargs):
738
+ """{}""" # noqa: D415
739
+ return _inspect(cls=self, values=values, field=field, **kwargs)
740
+
741
+ @doc_args(CanCurate.standardize.__doc__)
742
+ def standardize(
743
+ self, values: Iterable, field: str | StrField | None = None, **kwargs
744
+ ):
745
+ """{}""" # noqa: D415
746
+ return _standardize(cls=self, values=values, field=field, **kwargs)
747
+
748
+
749
+ # this differs from BasicQuerySet only in .filter and .get
750
+ # QueryManager returns BasicQuerySet because it is problematic to redefine .filter and .get
751
+ # for a query set used by the default manager
752
+ class QuerySet(BasicQuerySet):
753
+ """Sets of records returned by queries.
754
+
755
+ Implements additional filtering capabilities.
756
+
757
+ See Also:
758
+
759
+ `django QuerySet <https://docs.djangoproject.com/en/4.2/ref/models/querysets/>`__
760
+
761
+ Examples:
762
+
763
+ >>> ULabel(name="my label").save()
764
+ >>> queryset = ULabel.filter(name="my label")
765
+ >>> queryset # an instance of QuerySet
766
+ """
767
+
608
768
  def _handle_unknown_field(self, error: FieldError) -> None:
609
769
  """Suggest available fields if an unknown field was passed."""
610
770
  if "Cannot resolve keyword" in str(error):
@@ -657,88 +817,3 @@ class QuerySet(models.QuerySet):
657
817
  except FieldError as e:
658
818
  self._handle_unknown_field(e)
659
819
  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