lamindb 1.0.4__py3-none-any.whl → 1.1.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 +14 -5
- lamindb/_artifact.py +174 -57
- lamindb/_can_curate.py +27 -8
- lamindb/_collection.py +85 -51
- lamindb/_feature.py +177 -41
- lamindb/_finish.py +222 -81
- lamindb/_from_values.py +83 -98
- lamindb/_parents.py +4 -4
- lamindb/_query_set.py +59 -17
- lamindb/_record.py +171 -53
- lamindb/_run.py +4 -4
- lamindb/_save.py +33 -10
- lamindb/_schema.py +135 -38
- lamindb/_storage.py +1 -1
- lamindb/_tracked.py +106 -0
- lamindb/_transform.py +21 -8
- lamindb/_ulabel.py +5 -14
- lamindb/base/validation.py +2 -6
- lamindb/core/__init__.py +13 -14
- lamindb/core/_context.py +39 -36
- lamindb/core/_data.py +29 -25
- lamindb/core/_describe.py +1 -1
- lamindb/core/_django.py +1 -1
- lamindb/core/_feature_manager.py +54 -44
- lamindb/core/_label_manager.py +4 -4
- lamindb/core/_mapped_collection.py +20 -7
- lamindb/core/datasets/__init__.py +6 -1
- lamindb/core/datasets/_core.py +12 -11
- lamindb/core/datasets/_small.py +66 -20
- lamindb/core/exceptions.py +1 -90
- lamindb/core/loaders.py +7 -13
- lamindb/core/relations.py +6 -4
- lamindb/core/storage/_anndata_accessor.py +41 -0
- lamindb/core/storage/_backed_access.py +2 -2
- lamindb/core/storage/_pyarrow_dataset.py +25 -15
- lamindb/core/storage/_tiledbsoma.py +56 -12
- lamindb/core/storage/paths.py +41 -22
- lamindb/core/subsettings/_creation_settings.py +4 -16
- lamindb/curators/__init__.py +2168 -833
- lamindb/curators/_cellxgene_schemas/__init__.py +26 -0
- lamindb/curators/_cellxgene_schemas/schema_versions.yml +104 -0
- lamindb/errors.py +96 -0
- lamindb/integrations/_vitessce.py +3 -3
- lamindb/migrations/0069_squashed.py +76 -75
- lamindb/migrations/0075_lamindbv1_part5.py +4 -5
- lamindb/migrations/0082_alter_feature_dtype.py +21 -0
- lamindb/migrations/0083_alter_feature_is_type_alter_flextable_is_type_and_more.py +94 -0
- lamindb/migrations/0084_alter_schemafeature_feature_and_more.py +35 -0
- lamindb/migrations/0085_alter_feature_is_type_alter_flextable_is_type_and_more.py +63 -0
- lamindb/migrations/0086_various.py +95 -0
- lamindb/migrations/0087_rename__schemas_m2m_artifact_feature_sets_and_more.py +41 -0
- lamindb/migrations/0088_schema_components.py +273 -0
- lamindb/migrations/0088_squashed.py +4372 -0
- lamindb/models.py +423 -156
- {lamindb-1.0.4.dist-info → lamindb-1.1.0.dist-info}/METADATA +10 -7
- lamindb-1.1.0.dist-info/RECORD +95 -0
- lamindb/curators/_spatial.py +0 -528
- lamindb/migrations/0052_squashed.py +0 -1261
- lamindb/migrations/0053_alter_featureset_hash_alter_paramvalue_created_by_and_more.py +0 -57
- lamindb/migrations/0054_alter_feature_previous_runs_and_more.py +0 -35
- lamindb/migrations/0055_artifact_type_artifactparamvalue_and_more.py +0 -61
- lamindb/migrations/0056_rename_ulabel_ref_is_name_artifactulabel_label_ref_is_name_and_more.py +0 -22
- lamindb/migrations/0057_link_models_latest_report_and_others.py +0 -356
- lamindb/migrations/0058_artifact__actions_collection__actions.py +0 -22
- lamindb/migrations/0059_alter_artifact__accessor_alter_artifact__hash_type_and_more.py +0 -31
- lamindb/migrations/0060_alter_artifact__actions.py +0 -22
- lamindb/migrations/0061_alter_collection_meta_artifact_alter_run_environment_and_more.py +0 -45
- lamindb/migrations/0062_add_is_latest_field.py +0 -32
- lamindb/migrations/0063_populate_latest_field.py +0 -45
- lamindb/migrations/0064_alter_artifact_version_alter_collection_version_and_more.py +0 -33
- lamindb/migrations/0065_remove_collection_feature_sets_and_more.py +0 -22
- lamindb/migrations/0066_alter_artifact__feature_values_and_more.py +0 -352
- lamindb/migrations/0067_alter_featurevalue_unique_together_and_more.py +0 -20
- lamindb/migrations/0068_alter_artifactulabel_unique_together_and_more.py +0 -20
- lamindb/migrations/0069_alter_artifact__accessor_alter_artifact__hash_type_and_more.py +0 -1294
- lamindb-1.0.4.dist-info/RECORD +0 -102
- {lamindb-1.0.4.dist-info → lamindb-1.1.0.dist-info}/LICENSE +0 -0
- {lamindb-1.0.4.dist-info → lamindb-1.1.0.dist-info}/WHEEL +0 -0
lamindb/_from_values.py
CHANGED
@@ -9,8 +9,6 @@ from lamin_utils import colors, logger
|
|
9
9
|
from lamindb._query_set import RecordList
|
10
10
|
from lamindb.models import Record
|
11
11
|
|
12
|
-
from .core._settings import settings
|
13
|
-
|
14
12
|
if TYPE_CHECKING:
|
15
13
|
from collections.abc import Iterable
|
16
14
|
|
@@ -29,88 +27,72 @@ def get_or_create_records(
|
|
29
27
|
mute: bool = False,
|
30
28
|
) -> RecordList:
|
31
29
|
"""Get or create records from iterables."""
|
32
|
-
registry = field.field.model
|
30
|
+
registry = field.field.model # type: ignore
|
33
31
|
if create:
|
34
|
-
return RecordList([registry(**{field.field.name: value}) for value in iterable])
|
35
|
-
creation_search_names = settings.creation.search_names
|
32
|
+
return RecordList([registry(**{field.field.name: value}) for value in iterable]) # type: ignore
|
36
33
|
organism = _get_organism_record(field, organism)
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
mute=mute,
|
47
|
-
)
|
34
|
+
iterable_idx = index_iterable(iterable)
|
35
|
+
|
36
|
+
# returns existing records & non-existing values
|
37
|
+
records, nonexist_values, msg = get_existing_records(
|
38
|
+
iterable_idx=iterable_idx,
|
39
|
+
field=field,
|
40
|
+
organism=organism,
|
41
|
+
mute=mute,
|
42
|
+
)
|
48
43
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
44
|
+
# new records to be created based on new values
|
45
|
+
if len(nonexist_values) > 0:
|
46
|
+
source_record = None
|
47
|
+
if from_source:
|
48
|
+
if isinstance(source, Record):
|
49
|
+
source_record = source
|
50
|
+
if not source_record and hasattr(registry, "public"):
|
51
|
+
if organism is None:
|
52
|
+
organism = _ensembl_prefix(nonexist_values[0], field, organism)
|
53
|
+
organism = _get_organism_record(field, organism, force=True)
|
54
|
+
|
55
|
+
if source_record:
|
56
|
+
from bionty.core._add_ontology import check_source_in_db
|
57
|
+
|
58
|
+
check_source_in_db(registry=registry, source=source_record)
|
59
|
+
|
60
|
+
from_source = not source_record.in_db
|
61
|
+
elif hasattr(registry, "source_id"):
|
62
|
+
from_source = True
|
63
|
+
else:
|
64
|
+
from_source = False
|
65
|
+
|
66
|
+
if from_source:
|
67
|
+
records_bionty, unmapped_values = create_records_from_source(
|
68
|
+
iterable_idx=nonexist_values,
|
69
|
+
field=field,
|
70
|
+
organism=organism,
|
71
|
+
source=source_record,
|
72
|
+
msg=msg,
|
73
|
+
mute=mute,
|
74
|
+
)
|
75
|
+
if len(records_bionty) > 0:
|
76
|
+
msg = ""
|
77
|
+
for record in records_bionty:
|
78
|
+
record._from_source = True
|
79
|
+
records += records_bionty
|
80
|
+
else:
|
81
|
+
unmapped_values = nonexist_values
|
82
|
+
# unmapped new_ids will NOT create records
|
83
|
+
if len(unmapped_values) > 0:
|
84
|
+
if len(msg) > 0 and not mute:
|
85
|
+
logger.success(msg)
|
86
|
+
s = "" if len(unmapped_values) == 1 else "s"
|
87
|
+
print_values = colors.yellow(_format_values(unmapped_values))
|
88
|
+
name = registry.__name__
|
89
|
+
n_nonval = colors.yellow(f"{len(unmapped_values)} non-validated")
|
90
|
+
if not mute:
|
91
|
+
logger.warning(
|
92
|
+
f"{colors.red('did not create')} {name} record{s} for "
|
93
|
+
f"{n_nonval} {colors.italic(f'{field.field.name}{s}')}: {print_values}" # type: ignore
|
79
94
|
)
|
80
|
-
|
81
|
-
msg = ""
|
82
|
-
for record in records_bionty:
|
83
|
-
record._from_source = True
|
84
|
-
records += records_bionty
|
85
|
-
else:
|
86
|
-
unmapped_values = nonexist_values
|
87
|
-
# unmapped new_ids will NOT create records
|
88
|
-
if len(unmapped_values) > 0:
|
89
|
-
if len(msg) > 0 and not mute:
|
90
|
-
logger.success(msg)
|
91
|
-
s = "" if len(unmapped_values) == 1 else "s"
|
92
|
-
print_values = colors.yellow(_format_values(unmapped_values))
|
93
|
-
name = registry.__name__
|
94
|
-
n_nonval = colors.yellow(f"{len(unmapped_values)} non-validated")
|
95
|
-
if not mute:
|
96
|
-
logger.warning(
|
97
|
-
f"{colors.red('did not create')} {name} record{s} for "
|
98
|
-
f"{n_nonval} {colors.italic(f'{field.field.name}{s}')}: {print_values}"
|
99
|
-
)
|
100
|
-
# if registry.__get_module_name__() == "bionty" or registry == ULabel:
|
101
|
-
# if isinstance(iterable, pd.Series):
|
102
|
-
# feature = iterable.name
|
103
|
-
# feature_name = None
|
104
|
-
# if isinstance(feature, str):
|
105
|
-
# feature_name = feature
|
106
|
-
# if feature_name is not None:
|
107
|
-
# if feature_name is not None:
|
108
|
-
# for record in records:
|
109
|
-
# record._feature = feature_name
|
110
|
-
# logger.debug(f"added default feature '{feature_name}'")
|
111
|
-
return RecordList(records)
|
112
|
-
finally:
|
113
|
-
settings.creation.search_names = creation_search_names
|
95
|
+
return RecordList(records)
|
114
96
|
|
115
97
|
|
116
98
|
def get_existing_records(
|
@@ -120,10 +102,10 @@ def get_existing_records(
|
|
120
102
|
mute: bool = False,
|
121
103
|
):
|
122
104
|
# NOTE: existing records matching is agnostic to the source
|
123
|
-
model = field.field.model
|
124
|
-
if organism is None and field.field.name == "ensembl_gene_id":
|
105
|
+
model = field.field.model # type: ignore
|
106
|
+
if organism is None and field.field.name == "ensembl_gene_id": # type: ignore
|
125
107
|
if len(iterable_idx) > 0:
|
126
|
-
organism = _ensembl_prefix(iterable_idx[0], field, organism)
|
108
|
+
organism = _ensembl_prefix(iterable_idx[0], field, organism) # type: ignore
|
127
109
|
organism = _get_organism_record(field, organism, force=True)
|
128
110
|
|
129
111
|
# standardize based on the DB reference
|
@@ -152,6 +134,7 @@ def get_existing_records(
|
|
152
134
|
is_validated = model.validate(
|
153
135
|
iterable_idx, field=field, organism=organism, mute=True
|
154
136
|
)
|
137
|
+
|
155
138
|
if len(is_validated) > 0:
|
156
139
|
validated = iterable_idx[is_validated]
|
157
140
|
else:
|
@@ -165,7 +148,7 @@ def get_existing_records(
|
|
165
148
|
msg = (
|
166
149
|
"loaded"
|
167
150
|
f" {colors.green(f'{len(validated)} {model.__name__} record{s}')}"
|
168
|
-
f" matching {colors.italic(f'{field.field.name}')}: {print_values}"
|
151
|
+
f" matching {colors.italic(f'{field.field.name}')}: {print_values}" # type: ignore
|
169
152
|
)
|
170
153
|
if len(syn_mapper) > 0:
|
171
154
|
s = "" if len(syn_mapper) == 1 else "s"
|
@@ -189,7 +172,7 @@ def get_existing_records(
|
|
189
172
|
# get all existing records in the db
|
190
173
|
# if necessary, create records for the values in kwargs
|
191
174
|
# k:v -> k:v_record
|
192
|
-
query = {f"{field.field.name}__in": iterable_idx.values}
|
175
|
+
query = {f"{field.field.name}__in": iterable_idx.values} # type: ignore
|
193
176
|
if organism is not None:
|
194
177
|
query["organism"] = organism
|
195
178
|
records = model.filter(**query).list()
|
@@ -209,7 +192,7 @@ def create_records_from_source(
|
|
209
192
|
msg: str = "",
|
210
193
|
mute: bool = False,
|
211
194
|
):
|
212
|
-
model = field.field.model
|
195
|
+
model = field.field.model # type: ignore
|
213
196
|
records: list = []
|
214
197
|
# populate additional fields from bionty
|
215
198
|
from bionty._bionty import get_source_record
|
@@ -232,11 +215,11 @@ def create_records_from_source(
|
|
232
215
|
# standardize in the bionty reference
|
233
216
|
# do not inspect synonyms if the field is not name field
|
234
217
|
inspect_synonyms = True
|
235
|
-
if hasattr(model, "_name_field") and field.field.name != model._name_field:
|
218
|
+
if hasattr(model, "_name_field") and field.field.name != model._name_field: # type: ignore
|
236
219
|
inspect_synonyms = False
|
237
220
|
result = public_ontology.inspect(
|
238
221
|
iterable_idx,
|
239
|
-
field=field.field.name,
|
222
|
+
field=field.field.name, # type: ignore
|
240
223
|
mute=True,
|
241
224
|
inspect_synonyms=inspect_synonyms,
|
242
225
|
)
|
@@ -257,12 +240,14 @@ def create_records_from_source(
|
|
257
240
|
|
258
241
|
# create records for values that are found in the bionty reference
|
259
242
|
# matching either field or synonyms
|
260
|
-
mapped_values = iterable_idx.intersection(bionty_df[field.field.name])
|
243
|
+
mapped_values = iterable_idx.intersection(bionty_df[field.field.name]) # type: ignore
|
261
244
|
|
262
245
|
multi_msg = ""
|
263
246
|
if len(mapped_values) > 0:
|
264
247
|
bionty_kwargs, multi_msg = _bulk_create_dicts_from_df(
|
265
|
-
keys=mapped_values,
|
248
|
+
keys=mapped_values,
|
249
|
+
column_name=field.field.name, # type: ignore
|
250
|
+
df=bionty_df,
|
266
251
|
)
|
267
252
|
|
268
253
|
if hasattr(model, "organism_id") and organism is None:
|
@@ -274,7 +259,7 @@ def create_records_from_source(
|
|
274
259
|
else {"source": source}
|
275
260
|
)
|
276
261
|
for bk in bionty_kwargs:
|
277
|
-
records.append(model(**bk, **create_kwargs))
|
262
|
+
records.append(model(**bk, **create_kwargs, _skip_validation=True))
|
278
263
|
|
279
264
|
# number of records that matches field (not synonyms)
|
280
265
|
validated = result.validated
|
@@ -288,7 +273,7 @@ def create_records_from_source(
|
|
288
273
|
logger.success(
|
289
274
|
"created"
|
290
275
|
f" {colors.purple(f'{len(validated)} {model.__name__} record{s} from Bionty')}"
|
291
|
-
f" matching {colors.italic(f'{field.field.name}')}: {print_values}"
|
276
|
+
f" matching {colors.italic(f'{field.field.name}')}: {print_values}" # type: ignore
|
292
277
|
)
|
293
278
|
|
294
279
|
# make sure that synonyms logging appears after the field logging
|
@@ -365,7 +350,7 @@ def _has_organism_field(registry: type[Record]) -> bool:
|
|
365
350
|
return False
|
366
351
|
|
367
352
|
|
368
|
-
def _get_organism_record(
|
353
|
+
def _get_organism_record( # type: ignore
|
369
354
|
field: StrField, organism: str | Record, force: bool = False
|
370
355
|
) -> Record:
|
371
356
|
"""Get organism record.
|
@@ -375,10 +360,10 @@ def _get_organism_record(
|
|
375
360
|
organism: the organism to get the record for
|
376
361
|
force: whether to force fetching the organism record
|
377
362
|
"""
|
378
|
-
registry = field.field.model
|
363
|
+
registry = field.field.model # type: ignore
|
379
364
|
check = True
|
380
365
|
if not force and hasattr(registry, "_ontology_id_field"):
|
381
|
-
check = field.field.name != registry._ontology_id_field
|
366
|
+
check = field.field.name != registry._ontology_id_field # type: ignore
|
382
367
|
# e.g. bionty.CellMarker has "name" as _ontology_id_field
|
383
368
|
if not registry._ontology_id_field.endswith("id"):
|
384
369
|
check = True
|
@@ -397,10 +382,10 @@ def _get_organism_record(
|
|
397
382
|
|
398
383
|
|
399
384
|
def _ensembl_prefix(id: str, field: StrField, organism: Record | None) -> str | None:
|
400
|
-
if field.field.name == "ensembl_gene_id" and organism is None:
|
385
|
+
if field.field.name == "ensembl_gene_id" and organism is None: # type: ignore
|
401
386
|
if id.startswith("ENSG"):
|
402
|
-
organism = "human"
|
387
|
+
organism = "human" # type: ignore
|
403
388
|
elif id.startswith("ENSMUSG"):
|
404
|
-
organism = "mouse"
|
389
|
+
organism = "mouse" # type: ignore
|
405
390
|
|
406
391
|
return organism
|
lamindb/_parents.py
CHANGED
@@ -44,7 +44,7 @@ def _query_relatives(
|
|
44
44
|
kind: Literal["parents", "children"],
|
45
45
|
cls: type[HasParents],
|
46
46
|
) -> QuerySet:
|
47
|
-
relatives = cls.objects.none()
|
47
|
+
relatives = cls.objects.none() # type: ignore
|
48
48
|
if len(records) == 0:
|
49
49
|
return relatives
|
50
50
|
for record in records:
|
@@ -350,9 +350,9 @@ def _record_label(record: Record, field: str | None = None):
|
|
350
350
|
)
|
351
351
|
elif isinstance(record, Run):
|
352
352
|
if record.transform.description:
|
353
|
-
name = f
|
353
|
+
name = f"{record.transform.description.replace('&', '&')}"
|
354
354
|
elif record.transform.key:
|
355
|
-
name = f
|
355
|
+
name = f"{record.transform.key.replace('&', '&')}"
|
356
356
|
else:
|
357
357
|
name = f"{record.transform.uid}"
|
358
358
|
user_display = (
|
@@ -366,7 +366,7 @@ def _record_label(record: Record, field: str | None = None):
|
|
366
366
|
rf" user={user_display}<BR/>run={format_field_value(record.started_at)}</FONT>>"
|
367
367
|
)
|
368
368
|
elif isinstance(record, Transform):
|
369
|
-
name = f
|
369
|
+
name = f"{record.name.replace('&', '&')}"
|
370
370
|
return (
|
371
371
|
rf'<{TRANSFORM_EMOJIS.get(str(record.type), "💫")} {name}<BR/><FONT COLOR="GREY" POINT-SIZE="10"'
|
372
372
|
rf' FACE="Monospace">uid={record.uid}<BR/>type={record.type},'
|
lamindb/_query_set.py
CHANGED
@@ -8,6 +8,7 @@ from collections.abc import Iterable as IterableType
|
|
8
8
|
from typing import TYPE_CHECKING, Any, Generic, NamedTuple, TypeVar
|
9
9
|
|
10
10
|
import pandas as pd
|
11
|
+
from django.core.exceptions import FieldError
|
11
12
|
from django.db import models
|
12
13
|
from django.db.models import F, ForeignKey, ManyToManyField
|
13
14
|
from django.db.models.fields.related import ForeignObjectRel
|
@@ -26,7 +27,7 @@ from lamindb.models import (
|
|
26
27
|
Transform,
|
27
28
|
)
|
28
29
|
|
29
|
-
from .
|
30
|
+
from .errors import DoesNotExist
|
30
31
|
|
31
32
|
T = TypeVar("T")
|
32
33
|
|
@@ -91,14 +92,12 @@ def get_backward_compat_filter_kwargs(queryset, expressions):
|
|
91
92
|
"n_objects": "n_files",
|
92
93
|
"visibility": "_branch_code", # for convenience (and backward compat <1.0)
|
93
94
|
"transform": "run__transform", # for convenience (and backward compat <1.0)
|
94
|
-
"feature_sets": "_schemas_m2m",
|
95
95
|
"type": "kind",
|
96
96
|
"_accessor": "otype",
|
97
97
|
}
|
98
98
|
elif queryset.model == Schema:
|
99
99
|
name_mappings = {
|
100
100
|
"registry": "itype",
|
101
|
-
"artifacts": "_artifacts_m2m", # will raise warning when we start to migrate over
|
102
101
|
}
|
103
102
|
else:
|
104
103
|
return expressions
|
@@ -114,7 +113,6 @@ def get_backward_compat_filter_kwargs(queryset, expressions):
|
|
114
113
|
if parts[0] not in {
|
115
114
|
"transform",
|
116
115
|
"visibility",
|
117
|
-
"feature_sets",
|
118
116
|
"schemas",
|
119
117
|
"artifacts",
|
120
118
|
}:
|
@@ -203,7 +201,7 @@ def get(
|
|
203
201
|
qs = QuerySet(model=registry_or_queryset)
|
204
202
|
registry = registry_or_queryset
|
205
203
|
if isinstance(idlike, int):
|
206
|
-
return super(QuerySet, qs).get(id=idlike)
|
204
|
+
return super(QuerySet, qs).get(id=idlike) # type: ignore
|
207
205
|
elif isinstance(idlike, str):
|
208
206
|
qs = qs.filter(uid__startswith=idlike)
|
209
207
|
if issubclass(registry, IsVersioned):
|
@@ -216,6 +214,9 @@ def get(
|
|
216
214
|
else:
|
217
215
|
assert idlike is None # noqa: S101
|
218
216
|
expressions = process_expressions(qs, expressions)
|
217
|
+
# inject is_latest for consistency with idlike
|
218
|
+
if issubclass(registry, IsVersioned) and "is_latest" not in expressions:
|
219
|
+
expressions["is_latest"] = True
|
219
220
|
return registry.objects.using(qs.db).get(**expressions)
|
220
221
|
|
221
222
|
|
@@ -537,13 +538,13 @@ class QuerySet(models.QuerySet):
|
|
537
538
|
elif isinstance(include, str):
|
538
539
|
include = [include]
|
539
540
|
include = get_backward_compat_filter_kwargs(self, include)
|
540
|
-
field_names = get_basic_field_names(self, include, features)
|
541
|
+
field_names = get_basic_field_names(self, include, features) # type: ignore
|
541
542
|
|
542
543
|
annotate_kwargs = {}
|
543
544
|
if features:
|
544
545
|
annotate_kwargs.update(get_feature_annotate_kwargs(features))
|
545
546
|
if include:
|
546
|
-
include = include.copy()[::-1]
|
547
|
+
include = include.copy()[::-1] # type: ignore
|
547
548
|
include_kwargs = {s: F(s) for s in include if s not in field_names}
|
548
549
|
annotate_kwargs.update(include_kwargs)
|
549
550
|
if annotate_kwargs:
|
@@ -561,12 +562,6 @@ class QuerySet(models.QuerySet):
|
|
561
562
|
pk_column_name = pk_name if pk_name in df.columns else f"{pk_name}_id"
|
562
563
|
if pk_column_name in df_reshaped.columns:
|
563
564
|
df_reshaped = df_reshaped.set_index(pk_column_name)
|
564
|
-
|
565
|
-
# Compatibility code
|
566
|
-
df_reshaped.columns = df_reshaped.columns.str.replace(
|
567
|
-
r"_schemas_m2m", "feature_sets", regex=True
|
568
|
-
)
|
569
|
-
|
570
565
|
return df_reshaped
|
571
566
|
|
572
567
|
def delete(self, *args, **kwargs):
|
@@ -601,17 +596,64 @@ class QuerySet(models.QuerySet):
|
|
601
596
|
return None
|
602
597
|
return self[0]
|
603
598
|
|
599
|
+
def _handle_unknown_field(self, error: FieldError) -> None:
|
600
|
+
"""Suggest available fields if an unknown field was passed."""
|
601
|
+
if "Cannot resolve keyword" in str(error):
|
602
|
+
field = str(error).split("'")[1]
|
603
|
+
fields = ", ".join(
|
604
|
+
sorted(
|
605
|
+
f.name
|
606
|
+
for f in self.model._meta.get_fields()
|
607
|
+
if not f.name.startswith("_")
|
608
|
+
and not f.name.startswith("links_")
|
609
|
+
and not f.name.endswith("_id")
|
610
|
+
)
|
611
|
+
)
|
612
|
+
raise FieldError(
|
613
|
+
f"Unknown field '{field}'. Available fields: {fields}"
|
614
|
+
) from None
|
615
|
+
raise error # pragma: no cover
|
616
|
+
|
604
617
|
def get(self, idlike: int | str | None = None, **expressions) -> Record:
|
605
618
|
"""Query a single record. Raises error if there are more or none."""
|
606
|
-
|
619
|
+
try:
|
620
|
+
return get(self, idlike, **expressions)
|
621
|
+
except ValueError as e:
|
622
|
+
# Pass through original error for explicit id lookups
|
623
|
+
if "Field 'id' expected a number" in str(e):
|
624
|
+
if "id" in expressions:
|
625
|
+
raise
|
626
|
+
field = next(iter(expressions))
|
627
|
+
raise FieldError(
|
628
|
+
f"Invalid lookup '{expressions[field]}' for {field}. Did you mean {field}__name?"
|
629
|
+
) from None
|
630
|
+
raise # pragma: no cover
|
631
|
+
except FieldError as e:
|
632
|
+
self._handle_unknown_field(e)
|
633
|
+
raise # pragma: no cover
|
607
634
|
|
608
635
|
def filter(self, *queries, **expressions) -> QuerySet:
|
609
636
|
"""Query a set of records."""
|
637
|
+
# Suggest to use __name for related fields such as id when not passed
|
638
|
+
for field, value in expressions.items():
|
639
|
+
if (
|
640
|
+
isinstance(value, str)
|
641
|
+
and value.strip("-").isalpha()
|
642
|
+
and "__" not in field
|
643
|
+
and hasattr(self.model, field)
|
644
|
+
and getattr(self.model, field).field.related_model
|
645
|
+
):
|
646
|
+
raise FieldError(
|
647
|
+
f"Invalid lookup '{value}' for {field}. Did you mean {field}__name?"
|
648
|
+
)
|
649
|
+
|
610
650
|
expressions = process_expressions(self, expressions)
|
611
651
|
if len(expressions) > 0:
|
612
|
-
|
613
|
-
|
614
|
-
|
652
|
+
try:
|
653
|
+
return super().filter(*queries, **expressions)
|
654
|
+
except FieldError as e:
|
655
|
+
self._handle_unknown_field(e)
|
656
|
+
return self
|
615
657
|
|
616
658
|
def one(self) -> Record:
|
617
659
|
"""Exactly one result. Raises error if there are more or none."""
|