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.
- lamindb/__init__.py +52 -36
- lamindb/_finish.py +17 -10
- lamindb/_tracked.py +1 -1
- lamindb/base/__init__.py +3 -1
- lamindb/base/fields.py +40 -22
- lamindb/base/ids.py +1 -94
- lamindb/base/types.py +2 -0
- lamindb/base/uids.py +117 -0
- lamindb/core/_context.py +203 -102
- lamindb/core/_settings.py +38 -25
- lamindb/core/datasets/__init__.py +11 -4
- lamindb/core/datasets/_core.py +5 -5
- lamindb/core/datasets/_small.py +0 -93
- lamindb/core/datasets/mini_immuno.py +172 -0
- lamindb/core/loaders.py +1 -1
- lamindb/core/storage/_backed_access.py +100 -6
- lamindb/core/storage/_polars_lazy_df.py +51 -0
- lamindb/core/storage/_pyarrow_dataset.py +15 -30
- lamindb/core/storage/_tiledbsoma.py +29 -13
- lamindb/core/storage/objects.py +6 -0
- lamindb/core/subsettings/__init__.py +2 -0
- lamindb/core/subsettings/_annotation_settings.py +11 -0
- lamindb/curators/__init__.py +7 -3349
- lamindb/curators/_legacy.py +2056 -0
- lamindb/curators/core.py +1534 -0
- lamindb/errors.py +11 -0
- lamindb/examples/__init__.py +27 -0
- lamindb/examples/schemas/__init__.py +12 -0
- lamindb/examples/schemas/_anndata.py +25 -0
- lamindb/examples/schemas/_simple.py +19 -0
- lamindb/integrations/_vitessce.py +8 -5
- lamindb/migrations/0091_alter_featurevalue_options_alter_space_options_and_more.py +24 -0
- lamindb/migrations/0092_alter_artifactfeaturevalue_artifact_and_more.py +75 -0
- lamindb/migrations/0093_alter_schemacomponent_unique_together.py +16 -0
- lamindb/models/__init__.py +4 -1
- lamindb/models/_describe.py +21 -4
- lamindb/models/_feature_manager.py +382 -287
- lamindb/models/_label_manager.py +8 -2
- lamindb/models/artifact.py +177 -106
- lamindb/models/artifact_set.py +122 -0
- lamindb/models/collection.py +73 -52
- lamindb/models/core.py +1 -1
- lamindb/models/feature.py +51 -17
- lamindb/models/has_parents.py +69 -14
- lamindb/models/project.py +1 -1
- lamindb/models/query_manager.py +221 -22
- lamindb/models/query_set.py +247 -172
- lamindb/models/record.py +65 -247
- lamindb/models/run.py +4 -4
- lamindb/models/save.py +8 -2
- lamindb/models/schema.py +456 -184
- lamindb/models/transform.py +2 -2
- lamindb/models/ulabel.py +8 -5
- {lamindb-1.4.0.dist-info → lamindb-1.5.1.dist-info}/METADATA +6 -6
- {lamindb-1.4.0.dist-info → lamindb-1.5.1.dist-info}/RECORD +57 -43
- {lamindb-1.4.0.dist-info → lamindb-1.5.1.dist-info}/LICENSE +0 -0
- {lamindb-1.4.0.dist-info → lamindb-1.5.1.dist-info}/WHEEL +0 -0
lamindb/models/query_set.py
CHANGED
@@ -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 .
|
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,
|
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
|
275
|
-
|
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(
|
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
|
-
|
289
|
-
if isinstance(
|
290
|
-
|
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
|
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
|
-
|
390
|
-
|
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
|
422
|
+
"""Reshapes tidy table to wide format.
|
393
423
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
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
|
-
|
406
|
-
|
407
|
-
#
|
408
|
-
|
409
|
-
|
410
|
-
#
|
411
|
-
if
|
412
|
-
#
|
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
|
-
|
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
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
461
|
+
result = result.join(
|
462
|
+
feature_values.set_index("id"),
|
463
|
+
on="id",
|
464
|
+
)
|
421
465
|
|
422
|
-
#
|
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,
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
-
|
440
|
-
|
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
|
-
|
448
|
-
|
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
|
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(
|
537
|
+
result.insert(3, feature_name, result["id"].map(feature_values))
|
492
538
|
|
493
539
|
return result
|
494
540
|
|
495
541
|
|
496
|
-
def
|
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(
|
553
|
+
result.insert(3, col, result["id"].map(values))
|
508
554
|
|
509
555
|
return result
|
510
556
|
|
511
557
|
|
512
|
-
class
|
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/
|
563
|
+
`django QuerySet <https://docs.djangoproject.com/en/stable/ref/models/querysets/>`__
|
518
564
|
|
519
565
|
Examples:
|
520
566
|
|
521
|
-
|
522
|
-
|
523
|
-
|
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] =
|
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
|
-
|
596
|
+
include_input = []
|
535
597
|
elif isinstance(include, str):
|
536
|
-
|
537
|
-
|
538
|
-
|
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
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
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
|
-
|
567
|
-
|
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
|
-
|
665
|
+
super().delete(*args, **kwargs)
|
585
666
|
|
586
|
-
def list(self, field: str | None = None) -> list[Record]:
|
587
|
-
"""Populate
|
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
|