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.
- 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 +177 -89
- 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/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 +1546 -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/models/__init__.py +4 -1
- lamindb/models/_describe.py +21 -4
- lamindb/models/_feature_manager.py +365 -286
- lamindb/models/_label_manager.py +8 -2
- lamindb/models/artifact.py +173 -95
- 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 +2 -2
- lamindb/models/project.py +1 -1
- lamindb/models/query_manager.py +221 -22
- lamindb/models/query_set.py +245 -171
- lamindb/models/record.py +62 -243
- lamindb/models/run.py +4 -4
- lamindb/models/save.py +8 -2
- lamindb/models/schema.py +458 -181
- lamindb/models/transform.py +2 -2
- lamindb/models/ulabel.py +8 -5
- {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/METADATA +6 -6
- {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/RECORD +55 -42
- {lamindb-1.4.0.dist-info → lamindb-1.5.0.dist-info}/LICENSE +0 -0
- {lamindb-1.4.0.dist-info → lamindb-1.5.0.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,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
|
-
|
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
|
-
# 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
|
-
#
|
411
|
-
|
412
|
-
|
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
|
-
|
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
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
460
|
+
result = result.join(
|
461
|
+
feature_values.set_index("id"),
|
462
|
+
on="id",
|
463
|
+
)
|
421
464
|
|
422
|
-
#
|
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,
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
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
|
-
|
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)
|
494
|
+
# sort columns
|
495
|
+
result = reorder_subset_columns_in_df(result, feature_names)
|
446
496
|
|
447
|
-
|
448
|
-
|
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
|
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(
|
536
|
+
result.insert(3, feature_name, result["id"].map(feature_values))
|
492
537
|
|
493
538
|
return result
|
494
539
|
|
495
540
|
|
496
|
-
def
|
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(
|
552
|
+
result.insert(3, col, result["id"].map(values))
|
508
553
|
|
509
554
|
return result
|
510
555
|
|
511
556
|
|
512
|
-
class
|
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/
|
562
|
+
`django QuerySet <https://docs.djangoproject.com/en/stable/ref/models/querysets/>`__
|
518
563
|
|
519
564
|
Examples:
|
520
565
|
|
521
|
-
|
522
|
-
|
523
|
-
|
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] =
|
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
|
-
|
595
|
+
include_input = []
|
535
596
|
elif isinstance(include, str):
|
536
|
-
|
537
|
-
|
538
|
-
|
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
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
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
|
-
|
567
|
-
|
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
|
-
|
664
|
+
super().delete(*args, **kwargs)
|
585
665
|
|
586
|
-
def list(self, field: str | None = None) -> list[Record]:
|
587
|
-
"""Populate
|
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
|