lamindb 0.76.8__py3-none-any.whl → 0.76.10__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 +114 -113
- lamindb/_artifact.py +1206 -1205
- lamindb/_can_validate.py +621 -579
- lamindb/_collection.py +390 -387
- lamindb/_curate.py +1603 -1601
- lamindb/_feature.py +155 -155
- lamindb/_feature_set.py +244 -242
- lamindb/_filter.py +23 -23
- lamindb/_finish.py +250 -256
- lamindb/_from_values.py +403 -382
- lamindb/_is_versioned.py +40 -40
- lamindb/_parents.py +476 -476
- lamindb/_query_manager.py +125 -125
- lamindb/_query_set.py +364 -362
- lamindb/_record.py +668 -649
- lamindb/_run.py +60 -57
- lamindb/_save.py +310 -308
- lamindb/_storage.py +14 -14
- lamindb/_transform.py +130 -127
- lamindb/_ulabel.py +56 -56
- lamindb/_utils.py +9 -9
- lamindb/_view.py +72 -72
- lamindb/core/__init__.py +94 -94
- lamindb/core/_context.py +590 -574
- lamindb/core/_data.py +510 -438
- lamindb/core/_django.py +209 -0
- lamindb/core/_feature_manager.py +994 -867
- lamindb/core/_label_manager.py +289 -253
- lamindb/core/_mapped_collection.py +631 -597
- lamindb/core/_settings.py +188 -187
- lamindb/core/_sync_git.py +138 -138
- lamindb/core/_track_environment.py +27 -27
- lamindb/core/datasets/__init__.py +59 -59
- lamindb/core/datasets/_core.py +581 -571
- lamindb/core/datasets/_fake.py +36 -36
- lamindb/core/exceptions.py +90 -90
- lamindb/core/fields.py +12 -12
- lamindb/core/loaders.py +164 -164
- lamindb/core/schema.py +56 -56
- lamindb/core/storage/__init__.py +25 -25
- lamindb/core/storage/_anndata_accessor.py +741 -740
- lamindb/core/storage/_anndata_sizes.py +41 -41
- lamindb/core/storage/_backed_access.py +98 -98
- lamindb/core/storage/_tiledbsoma.py +204 -204
- lamindb/core/storage/_valid_suffixes.py +21 -21
- lamindb/core/storage/_zarr.py +110 -110
- lamindb/core/storage/objects.py +62 -62
- lamindb/core/storage/paths.py +172 -172
- lamindb/core/subsettings/__init__.py +12 -12
- lamindb/core/subsettings/_creation_settings.py +38 -38
- lamindb/core/subsettings/_transform_settings.py +21 -21
- lamindb/core/types.py +19 -19
- lamindb/core/versioning.py +146 -158
- lamindb/integrations/__init__.py +12 -12
- lamindb/integrations/_vitessce.py +107 -107
- lamindb/setup/__init__.py +14 -14
- lamindb/setup/core/__init__.py +4 -4
- {lamindb-0.76.8.dist-info → lamindb-0.76.10.dist-info}/LICENSE +201 -201
- {lamindb-0.76.8.dist-info → lamindb-0.76.10.dist-info}/METADATA +8 -8
- lamindb-0.76.10.dist-info/RECORD +61 -0
- {lamindb-0.76.8.dist-info → lamindb-0.76.10.dist-info}/WHEEL +1 -1
- lamindb-0.76.8.dist-info/RECORD +0 -60
lamindb/_record.py
CHANGED
@@ -1,649 +1,668 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import builtins
|
4
|
-
from typing import TYPE_CHECKING,
|
5
|
-
|
6
|
-
import dj_database_url
|
7
|
-
import lamindb_setup as ln_setup
|
8
|
-
from django.db import connections, transaction
|
9
|
-
from django.db.models import IntegerField, Manager, Q, QuerySet, Value
|
10
|
-
from lamin_utils import logger
|
11
|
-
from lamin_utils._lookup import Lookup
|
12
|
-
from lamindb_setup._connect_instance import
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
from
|
18
|
-
from
|
19
|
-
|
20
|
-
from .
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
if
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
""
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
def
|
176
|
-
cls,
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
)
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
)
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
cls,
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
)
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
#
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
return cls
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
)
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
load_instance_settings
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
):
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
if
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
if
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
record
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
#
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
if
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
self.
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
.
|
613
|
-
.
|
614
|
-
.
|
615
|
-
)
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import builtins
|
4
|
+
from typing import TYPE_CHECKING, NamedTuple
|
5
|
+
|
6
|
+
import dj_database_url
|
7
|
+
import lamindb_setup as ln_setup
|
8
|
+
from django.db import connections, transaction
|
9
|
+
from django.db.models import IntegerField, Manager, Q, QuerySet, Value
|
10
|
+
from lamin_utils import logger
|
11
|
+
from lamin_utils._lookup import Lookup
|
12
|
+
from lamindb_setup._connect_instance import (
|
13
|
+
get_owner_name_from_identifier,
|
14
|
+
load_instance_settings,
|
15
|
+
update_db_using_local,
|
16
|
+
)
|
17
|
+
from lamindb_setup.core._docs import doc_args
|
18
|
+
from lamindb_setup.core._hub_core import connect_instance
|
19
|
+
from lamindb_setup.core._settings_store import instance_settings_file
|
20
|
+
from lnschema_core.models import IsVersioned, Record, Run, Transform
|
21
|
+
|
22
|
+
from lamindb._utils import attach_func_to_class_method
|
23
|
+
from lamindb.core._settings import settings
|
24
|
+
|
25
|
+
if TYPE_CHECKING:
|
26
|
+
import pandas as pd
|
27
|
+
from lnschema_core.types import ListLike, StrField
|
28
|
+
|
29
|
+
|
30
|
+
IPYTHON = getattr(builtins, "__IPYTHON__", False)
|
31
|
+
|
32
|
+
|
33
|
+
def init_self_from_db(self: Record, existing_record: Record):
|
34
|
+
new_args = [
|
35
|
+
getattr(existing_record, field.attname) for field in self._meta.concrete_fields
|
36
|
+
]
|
37
|
+
super(self.__class__, self).__init__(*new_args)
|
38
|
+
self._state.adding = False # mimic from_db
|
39
|
+
self._state.db = "default"
|
40
|
+
|
41
|
+
|
42
|
+
def update_attributes(record: Record, attributes: dict[str, str]):
|
43
|
+
for key, value in attributes.items():
|
44
|
+
if getattr(record, key) != value:
|
45
|
+
logger.warning(f"updated {key} from {getattr(record, key)} to {value}")
|
46
|
+
setattr(record, key, value)
|
47
|
+
|
48
|
+
|
49
|
+
def validate_required_fields(record: Record, kwargs):
|
50
|
+
required_fields = {
|
51
|
+
k.name for k in record._meta.fields if not k.null and k.default is None
|
52
|
+
}
|
53
|
+
required_fields_not_passed = {k: None for k in required_fields if k not in kwargs}
|
54
|
+
kwargs.update(required_fields_not_passed)
|
55
|
+
missing_fields = [
|
56
|
+
k for k, v in kwargs.items() if v is None and k in required_fields
|
57
|
+
]
|
58
|
+
if missing_fields:
|
59
|
+
raise TypeError(f"{missing_fields} are required.")
|
60
|
+
|
61
|
+
|
62
|
+
def suggest_records_with_similar_names(record: Record, kwargs) -> bool:
|
63
|
+
"""Returns True if found exact match, otherwise False.
|
64
|
+
|
65
|
+
Logs similar matches if found.
|
66
|
+
"""
|
67
|
+
if kwargs.get("name") is None:
|
68
|
+
return False
|
69
|
+
queryset = _search(
|
70
|
+
record.__class__, kwargs["name"], field="name", truncate_words=True, limit=3
|
71
|
+
)
|
72
|
+
if not queryset.exists(): # empty queryset
|
73
|
+
return False
|
74
|
+
for alternative_record in queryset:
|
75
|
+
if alternative_record.name == kwargs["name"]:
|
76
|
+
return True
|
77
|
+
s, it, nots = ("", "it", "s") if len(queryset) == 1 else ("s", "one of them", "")
|
78
|
+
msg = f"record{s} with similar name{s} exist{nots}! did you mean to load {it}?"
|
79
|
+
if IPYTHON:
|
80
|
+
from IPython.display import display
|
81
|
+
|
82
|
+
logger.warning(f"{msg}")
|
83
|
+
if settings._verbosity_int >= 1:
|
84
|
+
display(queryset.df())
|
85
|
+
else:
|
86
|
+
logger.warning(f"{msg}\n{queryset}")
|
87
|
+
return False
|
88
|
+
|
89
|
+
|
90
|
+
def __init__(record: Record, *args, **kwargs):
|
91
|
+
if not args:
|
92
|
+
validate_required_fields(record, kwargs)
|
93
|
+
|
94
|
+
# do not search for names if an id is passed; this is important
|
95
|
+
# e.g. when synching ids from the notebook store to lamindb
|
96
|
+
has_consciously_provided_uid = False
|
97
|
+
if "_has_consciously_provided_uid" in kwargs:
|
98
|
+
has_consciously_provided_uid = kwargs.pop("_has_consciously_provided_uid")
|
99
|
+
if settings.creation.search_names and not has_consciously_provided_uid:
|
100
|
+
match = suggest_records_with_similar_names(record, kwargs)
|
101
|
+
if match:
|
102
|
+
if "version" in kwargs:
|
103
|
+
if kwargs["version"] is not None:
|
104
|
+
version_comment = " and version"
|
105
|
+
existing_record = record.__class__.filter(
|
106
|
+
name=kwargs["name"], version=kwargs["version"]
|
107
|
+
).one_or_none()
|
108
|
+
else:
|
109
|
+
# for a versioned record, an exact name match is not a
|
110
|
+
# criterion for retrieving a record in case `version`
|
111
|
+
# isn't passed - we'd always pull out many records with exactly the
|
112
|
+
# same name
|
113
|
+
existing_record = None
|
114
|
+
else:
|
115
|
+
version_comment = ""
|
116
|
+
existing_record = record.__class__.filter(
|
117
|
+
name=kwargs["name"]
|
118
|
+
).one_or_none()
|
119
|
+
if existing_record is not None:
|
120
|
+
logger.important(
|
121
|
+
f"returning existing {record.__class__.__name__} record with same"
|
122
|
+
f" name{version_comment}: '{kwargs['name']}'"
|
123
|
+
)
|
124
|
+
init_self_from_db(record, existing_record)
|
125
|
+
return None
|
126
|
+
super(Record, record).__init__(**kwargs)
|
127
|
+
elif len(args) != len(record._meta.concrete_fields):
|
128
|
+
raise ValueError("please provide keyword arguments, not plain arguments")
|
129
|
+
else:
|
130
|
+
# object is loaded from DB (**kwargs could be omitted below, I believe)
|
131
|
+
super(Record, record).__init__(*args, **kwargs)
|
132
|
+
|
133
|
+
|
134
|
+
@classmethod # type:ignore
|
135
|
+
@doc_args(Record.filter.__doc__)
|
136
|
+
def filter(cls, *queries, **expressions) -> QuerySet:
|
137
|
+
"""{}""" # noqa: D415
|
138
|
+
from lamindb._filter import filter
|
139
|
+
|
140
|
+
return filter(cls, *queries, **expressions)
|
141
|
+
|
142
|
+
|
143
|
+
@classmethod # type:ignore
|
144
|
+
@doc_args(Record.get.__doc__)
|
145
|
+
def get(
|
146
|
+
cls,
|
147
|
+
idlike: int | str | None = None,
|
148
|
+
**expressions,
|
149
|
+
) -> Record:
|
150
|
+
"""{}""" # noqa: D415
|
151
|
+
# this is the only place in which we need the lamindb queryset
|
152
|
+
# in this file; everywhere else it should be Django's
|
153
|
+
from lamindb._query_set import QuerySet
|
154
|
+
|
155
|
+
return QuerySet(model=cls).get(idlike, **expressions)
|
156
|
+
|
157
|
+
|
158
|
+
@classmethod # type:ignore
|
159
|
+
@doc_args(Record.df.__doc__)
|
160
|
+
def df(
|
161
|
+
cls,
|
162
|
+
include: str | list[str] | None = None,
|
163
|
+
join: str = "inner",
|
164
|
+
limit: int = 100,
|
165
|
+
) -> pd.DataFrame:
|
166
|
+
"""{}""" # noqa: D415
|
167
|
+
from lamindb._filter import filter
|
168
|
+
|
169
|
+
query_set = filter(cls)
|
170
|
+
if hasattr(cls, "updated_at"):
|
171
|
+
query_set = query_set.order_by("-updated_at")
|
172
|
+
return query_set[:limit].df(include=include, join=join)
|
173
|
+
|
174
|
+
|
175
|
+
def _search(
|
176
|
+
cls,
|
177
|
+
string: str,
|
178
|
+
*,
|
179
|
+
field: StrField | list[StrField] | None = None,
|
180
|
+
limit: int | None = 20,
|
181
|
+
case_sensitive: bool = False,
|
182
|
+
using_key: str | None = None,
|
183
|
+
truncate_words: bool = False,
|
184
|
+
) -> QuerySet:
|
185
|
+
input_queryset = _queryset(cls, using_key=using_key)
|
186
|
+
registry = input_queryset.model
|
187
|
+
if field is None:
|
188
|
+
fields = [
|
189
|
+
field.name
|
190
|
+
for field in registry._meta.fields
|
191
|
+
if field.get_internal_type() in {"CharField", "TextField"}
|
192
|
+
]
|
193
|
+
else:
|
194
|
+
if not isinstance(field, list):
|
195
|
+
fields_input = [field]
|
196
|
+
else:
|
197
|
+
fields_input = field
|
198
|
+
fields = []
|
199
|
+
for field in fields_input:
|
200
|
+
if not isinstance(field, str):
|
201
|
+
try:
|
202
|
+
fields.append(field.field.name)
|
203
|
+
except AttributeError as error:
|
204
|
+
raise TypeError(
|
205
|
+
"Please pass a Record string field, e.g., `CellType.name`!"
|
206
|
+
) from error
|
207
|
+
else:
|
208
|
+
fields.append(field)
|
209
|
+
|
210
|
+
# decompose search string
|
211
|
+
def truncate_word(word) -> str:
|
212
|
+
if len(word) > 5:
|
213
|
+
n_80_pct = int(len(word) * 0.8)
|
214
|
+
return word[:n_80_pct]
|
215
|
+
elif len(word) > 3:
|
216
|
+
return word[:3]
|
217
|
+
else:
|
218
|
+
return word
|
219
|
+
|
220
|
+
decomposed_string = str(string).split()
|
221
|
+
# add the entire string back
|
222
|
+
decomposed_string += [string]
|
223
|
+
for word in decomposed_string:
|
224
|
+
# will not search against words with 3 or fewer characters
|
225
|
+
if len(word) <= 3:
|
226
|
+
decomposed_string.remove(word)
|
227
|
+
if truncate_words:
|
228
|
+
decomposed_string = [truncate_word(word) for word in decomposed_string]
|
229
|
+
# construct the query
|
230
|
+
expression = Q()
|
231
|
+
case_sensitive_i = "" if case_sensitive else "i"
|
232
|
+
for field in fields:
|
233
|
+
for word in decomposed_string:
|
234
|
+
query = {f"{field}__{case_sensitive_i}contains": word}
|
235
|
+
expression |= Q(**query)
|
236
|
+
output_queryset = input_queryset.filter(expression)
|
237
|
+
# ensure exact matches are at the top
|
238
|
+
narrow_expression = Q()
|
239
|
+
for field in fields:
|
240
|
+
query = {f"{field}__{case_sensitive_i}contains": string}
|
241
|
+
narrow_expression |= Q(**query)
|
242
|
+
refined_output_queryset = output_queryset.filter(narrow_expression).annotate(
|
243
|
+
ordering=Value(1, output_field=IntegerField())
|
244
|
+
)
|
245
|
+
remaining_output_queryset = output_queryset.exclude(narrow_expression).annotate(
|
246
|
+
ordering=Value(2, output_field=IntegerField())
|
247
|
+
)
|
248
|
+
combined_queryset = refined_output_queryset.union(
|
249
|
+
remaining_output_queryset
|
250
|
+
).order_by("ordering")[:limit]
|
251
|
+
return combined_queryset
|
252
|
+
|
253
|
+
|
254
|
+
@classmethod # type: ignore
|
255
|
+
@doc_args(Record.search.__doc__)
|
256
|
+
def search(
|
257
|
+
cls,
|
258
|
+
string: str,
|
259
|
+
*,
|
260
|
+
field: StrField | None = None,
|
261
|
+
limit: int | None = 20,
|
262
|
+
case_sensitive: bool = False,
|
263
|
+
) -> QuerySet:
|
264
|
+
"""{}""" # noqa: D415
|
265
|
+
return _search(
|
266
|
+
cls=cls,
|
267
|
+
string=string,
|
268
|
+
field=field,
|
269
|
+
limit=limit,
|
270
|
+
case_sensitive=case_sensitive,
|
271
|
+
)
|
272
|
+
|
273
|
+
|
274
|
+
def _lookup(
|
275
|
+
cls,
|
276
|
+
field: StrField | None = None,
|
277
|
+
return_field: StrField | None = None,
|
278
|
+
using_key: str | None = None,
|
279
|
+
) -> NamedTuple:
|
280
|
+
"""{}""" # noqa: D415
|
281
|
+
queryset = _queryset(cls, using_key=using_key)
|
282
|
+
field = get_name_field(registry=queryset.model, field=field)
|
283
|
+
|
284
|
+
return Lookup(
|
285
|
+
records=queryset,
|
286
|
+
values=[i.get(field) for i in queryset.values()],
|
287
|
+
tuple_name=cls.__class__.__name__,
|
288
|
+
prefix="ln",
|
289
|
+
).lookup(
|
290
|
+
return_field=(
|
291
|
+
get_name_field(registry=queryset.model, field=return_field)
|
292
|
+
if return_field is not None
|
293
|
+
else None
|
294
|
+
)
|
295
|
+
)
|
296
|
+
|
297
|
+
|
298
|
+
@classmethod # type: ignore
|
299
|
+
@doc_args(Record.lookup.__doc__)
|
300
|
+
def lookup(
|
301
|
+
cls,
|
302
|
+
field: StrField | None = None,
|
303
|
+
return_field: StrField | None = None,
|
304
|
+
) -> NamedTuple:
|
305
|
+
"""{}""" # noqa: D415
|
306
|
+
return _lookup(cls=cls, field=field, return_field=return_field)
|
307
|
+
|
308
|
+
|
309
|
+
def get_name_field(
|
310
|
+
registry: type[Record] | QuerySet | Manager,
|
311
|
+
*,
|
312
|
+
field: str | StrField | None = None,
|
313
|
+
) -> str:
|
314
|
+
"""Get the 1st char or text field from the registry."""
|
315
|
+
if isinstance(registry, (QuerySet, Manager)):
|
316
|
+
registry = registry.model
|
317
|
+
model_field_names = [i.name for i in registry._meta.fields]
|
318
|
+
|
319
|
+
# set to default name field
|
320
|
+
if field is None:
|
321
|
+
if hasattr(registry, "_name_field"):
|
322
|
+
field = registry._meta.get_field(registry._name_field)
|
323
|
+
elif "name" in model_field_names:
|
324
|
+
field = registry._meta.get_field("name")
|
325
|
+
else:
|
326
|
+
# first char or text field that doesn't contain "id"
|
327
|
+
for i in registry._meta.fields:
|
328
|
+
if "id" in i.name:
|
329
|
+
continue
|
330
|
+
if i.get_internal_type() in {"CharField", "TextField"}:
|
331
|
+
field = i
|
332
|
+
break
|
333
|
+
|
334
|
+
# no default name field can be found
|
335
|
+
if field is None:
|
336
|
+
raise ValueError(
|
337
|
+
"please pass a Record string field, e.g., `CellType.name`!"
|
338
|
+
)
|
339
|
+
else:
|
340
|
+
field = field.name # type:ignore
|
341
|
+
if not isinstance(field, str):
|
342
|
+
try:
|
343
|
+
field = field.field.name
|
344
|
+
except AttributeError:
|
345
|
+
raise TypeError(
|
346
|
+
"please pass a Record string field, e.g., `CellType.name`!"
|
347
|
+
) from None
|
348
|
+
|
349
|
+
return field
|
350
|
+
|
351
|
+
|
352
|
+
def _queryset(cls: Record | QuerySet | Manager, using_key: str) -> QuerySet:
|
353
|
+
if isinstance(cls, (QuerySet, Manager)):
|
354
|
+
return cls.all()
|
355
|
+
elif using_key is None or using_key == "default":
|
356
|
+
return cls.objects.all()
|
357
|
+
else:
|
358
|
+
# using must be called on cls, otherwise the connection isn't found
|
359
|
+
return cls.using(using_key).all()
|
360
|
+
|
361
|
+
|
362
|
+
def add_db_connection(db: str, using: str):
|
363
|
+
db_config = dj_database_url.config(
|
364
|
+
default=db, conn_max_age=600, conn_health_checks=True
|
365
|
+
)
|
366
|
+
db_config["TIME_ZONE"] = "UTC"
|
367
|
+
db_config["OPTIONS"] = {}
|
368
|
+
db_config["AUTOCOMMIT"] = True
|
369
|
+
connections.settings[using] = db_config
|
370
|
+
|
371
|
+
|
372
|
+
@classmethod # type: ignore
|
373
|
+
@doc_args(Record.using.__doc__)
|
374
|
+
def using(
|
375
|
+
cls,
|
376
|
+
instance: str | None,
|
377
|
+
) -> QuerySet:
|
378
|
+
"""{}""" # noqa: D415
|
379
|
+
if instance is None:
|
380
|
+
return QuerySet(model=cls, using=None)
|
381
|
+
owner, name = get_owner_name_from_identifier(instance)
|
382
|
+
settings_file = instance_settings_file(name, owner)
|
383
|
+
cache_filepath = (
|
384
|
+
ln_setup.settings.storage.cache_dir / f"instance--{owner}--{name}--uid.txt"
|
385
|
+
)
|
386
|
+
if not settings_file.exists():
|
387
|
+
result = connect_instance(owner=owner, name=name)
|
388
|
+
if isinstance(result, str):
|
389
|
+
raise RuntimeError(
|
390
|
+
f"Failed to load instance {instance}, please check your permissions!"
|
391
|
+
)
|
392
|
+
iresult, _ = result
|
393
|
+
source_schema = {
|
394
|
+
schema for schema in iresult["schema_str"].split(",") if schema != ""
|
395
|
+
} # type: ignore
|
396
|
+
target_schema = ln_setup.settings.instance.schema
|
397
|
+
if not source_schema.issubset(target_schema):
|
398
|
+
missing_members = source_schema - target_schema
|
399
|
+
logger.warning(
|
400
|
+
f"source schema has additional modules: {missing_members}\nconsider mounting these schema modules to not encounter errors"
|
401
|
+
)
|
402
|
+
cache_filepath.write_text(iresult["lnid"]) # type: ignore
|
403
|
+
settings_file = instance_settings_file(name, owner)
|
404
|
+
db = update_db_using_local(iresult, settings_file)
|
405
|
+
else:
|
406
|
+
isettings = load_instance_settings(settings_file)
|
407
|
+
db = isettings.db
|
408
|
+
cache_filepath.write_text(isettings.uid)
|
409
|
+
add_db_connection(db, instance)
|
410
|
+
return QuerySet(model=cls, using=instance)
|
411
|
+
|
412
|
+
|
413
|
+
REGISTRY_UNIQUE_FIELD = {
|
414
|
+
"storage": "root",
|
415
|
+
"feature": "name",
|
416
|
+
"ulabel": "name",
|
417
|
+
}
|
418
|
+
|
419
|
+
|
420
|
+
def update_fk_to_default_db(
|
421
|
+
records: Record | list[Record] | QuerySet,
|
422
|
+
fk: str,
|
423
|
+
using_key: str | None,
|
424
|
+
transfer_logs: dict,
|
425
|
+
):
|
426
|
+
record = records[0] if isinstance(records, (list, QuerySet)) else records
|
427
|
+
if hasattr(record, f"{fk}_id") and getattr(record, f"{fk}_id") is not None:
|
428
|
+
fk_record = getattr(record, fk)
|
429
|
+
field = REGISTRY_UNIQUE_FIELD.get(fk, "uid")
|
430
|
+
fk_record_default = fk_record.__class__.filter(
|
431
|
+
**{field: getattr(fk_record, field)}
|
432
|
+
).one_or_none()
|
433
|
+
if fk_record_default is None:
|
434
|
+
from copy import copy
|
435
|
+
|
436
|
+
fk_record_default = copy(fk_record)
|
437
|
+
transfer_to_default_db(
|
438
|
+
fk_record_default, using_key, save=True, transfer_logs=transfer_logs
|
439
|
+
)
|
440
|
+
if isinstance(records, (list, QuerySet)):
|
441
|
+
for r in records:
|
442
|
+
setattr(r, f"{fk}", None)
|
443
|
+
setattr(r, f"{fk}_id", fk_record_default.id)
|
444
|
+
else:
|
445
|
+
setattr(records, f"{fk}", None)
|
446
|
+
setattr(records, f"{fk}_id", fk_record_default.id)
|
447
|
+
|
448
|
+
|
449
|
+
FKBULK = [
|
450
|
+
"organism",
|
451
|
+
"source",
|
452
|
+
"_source_code_artifact", # Transform
|
453
|
+
"report", # Run
|
454
|
+
]
|
455
|
+
|
456
|
+
|
457
|
+
def transfer_fk_to_default_db_bulk(
|
458
|
+
records: list | QuerySet, using_key: str | None, transfer_logs: dict
|
459
|
+
):
|
460
|
+
for fk in FKBULK:
|
461
|
+
update_fk_to_default_db(records, fk, using_key, transfer_logs=transfer_logs)
|
462
|
+
|
463
|
+
|
464
|
+
def get_transfer_run(record) -> Run:
|
465
|
+
from lamindb_setup import settings as setup_settings
|
466
|
+
|
467
|
+
from lamindb.core._context import context
|
468
|
+
from lamindb.core._data import WARNING_RUN_TRANSFORM
|
469
|
+
|
470
|
+
slug = record._state.db
|
471
|
+
owner, name = get_owner_name_from_identifier(slug)
|
472
|
+
cache_filepath = (
|
473
|
+
ln_setup.settings.storage.cache_dir / f"instance--{owner}--{name}--uid.txt"
|
474
|
+
)
|
475
|
+
if not cache_filepath.exists():
|
476
|
+
raise SystemExit("Need to call .using() before")
|
477
|
+
instance_uid = cache_filepath.read_text()
|
478
|
+
key = f"transfers/{instance_uid}"
|
479
|
+
uid = instance_uid + "0000"
|
480
|
+
transform = Transform.filter(uid=uid).one_or_none()
|
481
|
+
if transform is None:
|
482
|
+
search_names = settings.creation.search_names
|
483
|
+
settings.creation.search_names = False
|
484
|
+
transform = Transform(
|
485
|
+
uid=uid, name=f"Transfer from `{slug}`", key=key, type="function"
|
486
|
+
).save()
|
487
|
+
settings.creation.search_names = search_names
|
488
|
+
# use the global run context to get the parent run id
|
489
|
+
if context.run is not None:
|
490
|
+
parent = context.run
|
491
|
+
else:
|
492
|
+
if not settings.creation.artifact_silence_missing_run_warning:
|
493
|
+
logger.warning(WARNING_RUN_TRANSFORM)
|
494
|
+
parent = None
|
495
|
+
# it doesn't seem to make sense to create new runs for every transfer
|
496
|
+
run = Run.filter(transform=transform, parent=parent).one_or_none()
|
497
|
+
if run is None:
|
498
|
+
run = Run(transform=transform, parent=parent).save()
|
499
|
+
run.parent = parent # so that it's available in memory
|
500
|
+
return run
|
501
|
+
|
502
|
+
|
503
|
+
def transfer_to_default_db(
|
504
|
+
record: Record,
|
505
|
+
using_key: str | None,
|
506
|
+
*,
|
507
|
+
transfer_logs: dict,
|
508
|
+
save: bool = False,
|
509
|
+
transfer_fk: bool = True,
|
510
|
+
) -> Record | None:
|
511
|
+
if record._state.db is None or record._state.db == "default":
|
512
|
+
return None
|
513
|
+
registry = record.__class__
|
514
|
+
record_on_default = registry.objects.filter(uid=record.uid).one_or_none()
|
515
|
+
record_str = f"{record.__class__.__name__}(uid='{record.uid}')"
|
516
|
+
if transfer_logs["run"] is None:
|
517
|
+
transfer_logs["run"] = get_transfer_run(record)
|
518
|
+
if record_on_default is not None:
|
519
|
+
transfer_logs["mapped"].append(record_str)
|
520
|
+
return record_on_default
|
521
|
+
else:
|
522
|
+
transfer_logs["transferred"].append(record_str)
|
523
|
+
|
524
|
+
if hasattr(record, "created_by_id"):
|
525
|
+
record.created_by = None
|
526
|
+
record.created_by_id = ln_setup.settings.user.id
|
527
|
+
# run & transform
|
528
|
+
run = transfer_logs["run"]
|
529
|
+
if hasattr(record, "run_id"):
|
530
|
+
record.run = None
|
531
|
+
record.run_id = run.id
|
532
|
+
# deal with denormalized transform FK on artifact and collection
|
533
|
+
if hasattr(record, "transform_id"):
|
534
|
+
record.transform = None
|
535
|
+
record.transform_id = run.transform_id
|
536
|
+
# transfer other foreign key fields
|
537
|
+
fk_fields = [
|
538
|
+
i.name
|
539
|
+
for i in record._meta.fields
|
540
|
+
if i.get_internal_type() == "ForeignKey"
|
541
|
+
if i.name not in {"created_by", "run", "transform"}
|
542
|
+
]
|
543
|
+
if not transfer_fk:
|
544
|
+
# don't transfer fk fields that are already bulk transferred
|
545
|
+
fk_fields = [fk for fk in fk_fields if fk not in FKBULK]
|
546
|
+
for fk in fk_fields:
|
547
|
+
update_fk_to_default_db(record, fk, using_key, transfer_logs=transfer_logs)
|
548
|
+
record.id = None
|
549
|
+
record._state.db = "default"
|
550
|
+
if save:
|
551
|
+
record.save()
|
552
|
+
return None
|
553
|
+
|
554
|
+
|
555
|
+
# docstring handled through attach_func_to_class_method
|
556
|
+
def save(self, *args, **kwargs) -> Record:
|
557
|
+
using_key = None
|
558
|
+
if "using" in kwargs:
|
559
|
+
using_key = kwargs["using"]
|
560
|
+
db = self._state.db
|
561
|
+
pk_on_db = self.pk
|
562
|
+
artifacts: list = []
|
563
|
+
if self.__class__.__name__ == "Collection" and self.id is not None:
|
564
|
+
# when creating a new collection without being able to access artifacts
|
565
|
+
artifacts = self.ordered_artifacts.list()
|
566
|
+
pre_existing_record = None
|
567
|
+
# consider records that are being transferred from other databases
|
568
|
+
transfer_logs: dict[str, list[str]] = {"mapped": [], "transferred": [], "run": None}
|
569
|
+
if db is not None and db != "default" and using_key is None:
|
570
|
+
if isinstance(self, IsVersioned):
|
571
|
+
if not self.is_latest:
|
572
|
+
raise NotImplementedError(
|
573
|
+
"You are attempting to transfer a record that's not the latest in its version history. This is currently not supported."
|
574
|
+
)
|
575
|
+
pre_existing_record = transfer_to_default_db(
|
576
|
+
self, using_key, transfer_logs=transfer_logs
|
577
|
+
)
|
578
|
+
if pre_existing_record is not None:
|
579
|
+
init_self_from_db(self, pre_existing_record)
|
580
|
+
else:
|
581
|
+
# save versioned record
|
582
|
+
if isinstance(self, IsVersioned) and self._revises is not None:
|
583
|
+
assert self._revises.is_latest # noqa: S101
|
584
|
+
revises = self._revises
|
585
|
+
revises.is_latest = False
|
586
|
+
with transaction.atomic():
|
587
|
+
revises._revises = None # ensure we don't start a recursion
|
588
|
+
revises.save()
|
589
|
+
super(Record, self).save(*args, **kwargs)
|
590
|
+
self._revises = None
|
591
|
+
# save unversioned record
|
592
|
+
else:
|
593
|
+
super(Record, self).save(*args, **kwargs)
|
594
|
+
# perform transfer of many-to-many fields
|
595
|
+
# only supported for Artifact and Collection records
|
596
|
+
if db is not None and db != "default" and using_key is None:
|
597
|
+
if self.__class__.__name__ == "Collection":
|
598
|
+
if len(artifacts) > 0:
|
599
|
+
logger.info("transfer artifacts")
|
600
|
+
for artifact in artifacts:
|
601
|
+
artifact.save()
|
602
|
+
self.artifacts.add(*artifacts)
|
603
|
+
if hasattr(self, "labels"):
|
604
|
+
from copy import copy
|
605
|
+
|
606
|
+
from lnschema_core.models import FeatureManager
|
607
|
+
|
608
|
+
# here we go back to original record on the source database
|
609
|
+
self_on_db = copy(self)
|
610
|
+
self_on_db._state.db = db
|
611
|
+
self_on_db.pk = pk_on_db # manually set the primary key
|
612
|
+
self_on_db.features = FeatureManager(self_on_db)
|
613
|
+
self.features._add_from(self_on_db, transfer_logs=transfer_logs)
|
614
|
+
self.labels.add_from(self_on_db, transfer_logs=transfer_logs)
|
615
|
+
for k, v in transfer_logs.items():
|
616
|
+
if k != "run":
|
617
|
+
logger.important(f"{k} records: {', '.join(v)}")
|
618
|
+
return self
|
619
|
+
|
620
|
+
|
621
|
+
def delete(self) -> None:
|
622
|
+
"""Delete the record."""
|
623
|
+
# note that the logic below does not fire if a record is moved to the trash
|
624
|
+
# the idea is that moving a record to the trash should move its entire version family
|
625
|
+
# to the trash, whereas permanently deleting should default to only deleting a single record
|
626
|
+
# of a version family
|
627
|
+
# we can consider making it easy to permanently delete entire version families as well,
|
628
|
+
# but that's for another time
|
629
|
+
if isinstance(self, IsVersioned) and self.is_latest:
|
630
|
+
new_latest = (
|
631
|
+
self.__class__.objects.using(self._state.db)
|
632
|
+
.filter(is_latest=False, uid__startswith=self.stem_uid)
|
633
|
+
.order_by("-created_at")
|
634
|
+
.first()
|
635
|
+
)
|
636
|
+
if new_latest is not None:
|
637
|
+
new_latest.is_latest = True
|
638
|
+
with transaction.atomic():
|
639
|
+
new_latest.save()
|
640
|
+
super(Record, self).delete()
|
641
|
+
logger.warning(f"new latest version is {new_latest}")
|
642
|
+
return None
|
643
|
+
super(Record, self).delete()
|
644
|
+
|
645
|
+
|
646
|
+
METHOD_NAMES = [
|
647
|
+
"__init__",
|
648
|
+
"filter",
|
649
|
+
"get",
|
650
|
+
"df",
|
651
|
+
"search",
|
652
|
+
"lookup",
|
653
|
+
"save",
|
654
|
+
"delete",
|
655
|
+
"using",
|
656
|
+
]
|
657
|
+
|
658
|
+
if ln_setup._TESTING: # type: ignore
|
659
|
+
from inspect import signature
|
660
|
+
|
661
|
+
SIGS = {
|
662
|
+
name: signature(getattr(Record, name))
|
663
|
+
for name in METHOD_NAMES
|
664
|
+
if not name.startswith("__")
|
665
|
+
}
|
666
|
+
|
667
|
+
for name in METHOD_NAMES:
|
668
|
+
attach_func_to_class_method(name, Record, globals())
|