lamindb_setup 1.9.1__py3-none-any.whl → 1.10.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_setup/__init__.py +107 -107
- lamindb_setup/_cache.py +87 -87
- lamindb_setup/_check_setup.py +192 -166
- lamindb_setup/_connect_instance.py +430 -328
- lamindb_setup/_delete.py +144 -141
- lamindb_setup/_disconnect.py +35 -32
- lamindb_setup/_init_instance.py +430 -440
- lamindb_setup/_migrate.py +278 -266
- lamindb_setup/_register_instance.py +32 -35
- lamindb_setup/_schema_metadata.py +441 -441
- lamindb_setup/_set_managed_storage.py +69 -70
- lamindb_setup/_setup_user.py +172 -133
- lamindb_setup/core/__init__.py +21 -21
- lamindb_setup/core/_aws_options.py +223 -223
- lamindb_setup/core/_aws_storage.py +9 -1
- lamindb_setup/core/_deprecated.py +1 -1
- lamindb_setup/core/_hub_client.py +248 -248
- lamindb_setup/core/_hub_core.py +751 -665
- lamindb_setup/core/_hub_crud.py +247 -227
- lamindb_setup/core/_private_django_api.py +83 -83
- lamindb_setup/core/_settings.py +374 -377
- lamindb_setup/core/_settings_instance.py +609 -569
- lamindb_setup/core/_settings_load.py +141 -141
- lamindb_setup/core/_settings_save.py +95 -95
- lamindb_setup/core/_settings_storage.py +427 -429
- lamindb_setup/core/_settings_store.py +91 -91
- lamindb_setup/core/_settings_user.py +55 -55
- lamindb_setup/core/_setup_bionty_sources.py +44 -44
- lamindb_setup/core/cloud_sqlite_locker.py +240 -240
- lamindb_setup/core/django.py +311 -305
- lamindb_setup/core/exceptions.py +1 -1
- lamindb_setup/core/hashing.py +134 -134
- lamindb_setup/core/types.py +1 -1
- lamindb_setup/core/upath.py +1013 -1013
- lamindb_setup/errors.py +80 -70
- lamindb_setup/types.py +20 -20
- {lamindb_setup-1.9.1.dist-info → lamindb_setup-1.10.1.dist-info}/METADATA +4 -4
- lamindb_setup-1.10.1.dist-info/RECORD +50 -0
- lamindb_setup-1.9.1.dist-info/RECORD +0 -50
- {lamindb_setup-1.9.1.dist-info → lamindb_setup-1.10.1.dist-info}/LICENSE +0 -0
- {lamindb_setup-1.9.1.dist-info → lamindb_setup-1.10.1.dist-info}/WHEEL +0 -0
|
@@ -1,569 +1,609 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
import shutil
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import TYPE_CHECKING, Literal
|
|
7
|
-
|
|
8
|
-
from django.db
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from .
|
|
13
|
-
from .
|
|
14
|
-
from .
|
|
15
|
-
from .
|
|
16
|
-
from .
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
from
|
|
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
|
-
self.
|
|
91
|
-
self.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
self.
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
@property
|
|
335
|
-
def
|
|
336
|
-
"""
|
|
337
|
-
return self.
|
|
338
|
-
|
|
339
|
-
@property
|
|
340
|
-
def
|
|
341
|
-
"""
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
"""
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
@property
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def
|
|
390
|
-
"""
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
f"
|
|
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
|
-
if
|
|
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
|
-
if self.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
if
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Literal
|
|
7
|
+
|
|
8
|
+
from django.db import connection
|
|
9
|
+
from django.db.utils import ProgrammingError
|
|
10
|
+
from lamin_utils import logger
|
|
11
|
+
|
|
12
|
+
from ._deprecated import deprecated
|
|
13
|
+
from ._hub_client import call_with_fallback
|
|
14
|
+
from ._hub_crud import select_account_handle_name_by_lnid
|
|
15
|
+
from ._hub_utils import LaminDsn, LaminDsnModel
|
|
16
|
+
from ._settings_save import save_instance_settings
|
|
17
|
+
from ._settings_storage import (
|
|
18
|
+
LEGACY_STORAGE_UID_FILE_KEY,
|
|
19
|
+
STORAGE_UID_FILE_KEY,
|
|
20
|
+
StorageSettings,
|
|
21
|
+
get_storage_type,
|
|
22
|
+
init_storage,
|
|
23
|
+
instance_uid_from_uuid,
|
|
24
|
+
)
|
|
25
|
+
from ._settings_store import current_instance_settings_file, instance_settings_file
|
|
26
|
+
from .cloud_sqlite_locker import (
|
|
27
|
+
EXPIRATION_TIME,
|
|
28
|
+
InstanceLockedException,
|
|
29
|
+
)
|
|
30
|
+
from .upath import LocalPathClasses, UPath
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from uuid import UUID
|
|
34
|
+
|
|
35
|
+
from ._settings_user import UserSettings
|
|
36
|
+
from .types import UPathStr
|
|
37
|
+
|
|
38
|
+
LOCAL_STORAGE_MESSAGE = "No local storage location found in current environment: defaulting to cloud storage"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def sanitize_git_repo_url(repo_url: str) -> str:
|
|
42
|
+
assert repo_url.startswith("https://")
|
|
43
|
+
return repo_url.replace(".git", "")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def is_local_db_url(db_url: str) -> bool:
|
|
47
|
+
if "@localhost:" in db_url:
|
|
48
|
+
return True
|
|
49
|
+
if "@0.0.0.0:" in db_url:
|
|
50
|
+
return True
|
|
51
|
+
if "@127.0.0.1" in db_url:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_is_instance_remote(root: UPathStr, db: str | None) -> bool:
|
|
57
|
+
# returns True for cloud SQLite
|
|
58
|
+
# and remote postgres
|
|
59
|
+
root_str = str(root)
|
|
60
|
+
if not root_str.startswith("create-s3") and get_storage_type(root_str) == "local":
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
if db is not None and is_local_db_url(db):
|
|
64
|
+
return False
|
|
65
|
+
return True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InstanceSettings:
|
|
69
|
+
"""Instance settings."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
id: UUID, # instance id/uuid
|
|
74
|
+
owner: str, # owner handle
|
|
75
|
+
name: str, # instance name
|
|
76
|
+
storage: StorageSettings | None = None, # storage location
|
|
77
|
+
keep_artifacts_local: bool = False, # default to local storage
|
|
78
|
+
db: str | None = None, # DB URI
|
|
79
|
+
modules: str | None = None, # comma-separated string of module names
|
|
80
|
+
git_repo: str | None = None, # a git repo URL
|
|
81
|
+
is_on_hub: bool | None = None, # initialized from hub
|
|
82
|
+
api_url: str | None = None,
|
|
83
|
+
schema_id: UUID | None = None,
|
|
84
|
+
fine_grained_access: bool = False,
|
|
85
|
+
db_permissions: str | None = None,
|
|
86
|
+
_locker_user: UserSettings | None = None, # user to lock for if cloud sqlite
|
|
87
|
+
):
|
|
88
|
+
from ._hub_utils import validate_db_arg
|
|
89
|
+
|
|
90
|
+
self._id_: UUID = id
|
|
91
|
+
self._owner: str = owner
|
|
92
|
+
self._name: str = name
|
|
93
|
+
self._storage: StorageSettings | None = storage
|
|
94
|
+
validate_db_arg(db)
|
|
95
|
+
self._db: str | None = db
|
|
96
|
+
self._schema_str: str | None = modules
|
|
97
|
+
self._git_repo = None if git_repo is None else sanitize_git_repo_url(git_repo)
|
|
98
|
+
# local storage
|
|
99
|
+
self._keep_artifacts_local = keep_artifacts_local
|
|
100
|
+
self._local_storage: StorageSettings | None = None
|
|
101
|
+
self._is_on_hub = is_on_hub
|
|
102
|
+
# private, needed for api requests
|
|
103
|
+
self._api_url = api_url
|
|
104
|
+
self._schema_id = schema_id
|
|
105
|
+
# private, whether fine grained access is used
|
|
106
|
+
# needed to be set to request jwt etc
|
|
107
|
+
self._fine_grained_access = fine_grained_access
|
|
108
|
+
# permissions for db such as jwt, read, write etc.
|
|
109
|
+
self._db_permissions = db_permissions
|
|
110
|
+
# if None then settings.user is used
|
|
111
|
+
self._locker_user = _locker_user
|
|
112
|
+
|
|
113
|
+
def __repr__(self):
|
|
114
|
+
"""Rich string representation."""
|
|
115
|
+
representation = "Current instance:"
|
|
116
|
+
attrs = ["slug", "storage", "db", "modules", "git_repo"]
|
|
117
|
+
for attr in attrs:
|
|
118
|
+
value = getattr(self, attr)
|
|
119
|
+
if attr == "storage":
|
|
120
|
+
if self.keep_artifacts_local:
|
|
121
|
+
import lamindb as ln
|
|
122
|
+
|
|
123
|
+
self._local_storage = ln.setup.settings.instance._local_storage
|
|
124
|
+
if self._local_storage is not None:
|
|
125
|
+
value_local = self.local_storage
|
|
126
|
+
representation += f"\n - local storage: {value_local.root_as_str} ({value_local.region})"
|
|
127
|
+
representation += (
|
|
128
|
+
f"\n - cloud storage: {value.root_as_str} ({value.region})"
|
|
129
|
+
)
|
|
130
|
+
else:
|
|
131
|
+
representation += (
|
|
132
|
+
f"\n - storage: {value.root_as_str} ({value.region})"
|
|
133
|
+
)
|
|
134
|
+
elif attr == "db":
|
|
135
|
+
if self.dialect != "sqlite":
|
|
136
|
+
model = LaminDsnModel(db=value)
|
|
137
|
+
db_print = LaminDsn.build(
|
|
138
|
+
scheme=model.db.scheme,
|
|
139
|
+
user=model.db.user,
|
|
140
|
+
password="***",
|
|
141
|
+
host="***",
|
|
142
|
+
port=model.db.port,
|
|
143
|
+
database=model.db.database,
|
|
144
|
+
)
|
|
145
|
+
else:
|
|
146
|
+
db_print = value
|
|
147
|
+
representation += f"\n - {attr}: {db_print}"
|
|
148
|
+
elif attr == "modules":
|
|
149
|
+
representation += f"\n - {attr}: {value if value else '{}'}"
|
|
150
|
+
else:
|
|
151
|
+
representation += f"\n - {attr}: {value}"
|
|
152
|
+
return representation
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def owner(self) -> str:
|
|
156
|
+
"""Instance owner. A user or organization account handle."""
|
|
157
|
+
return self._owner
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def name(self) -> str:
|
|
161
|
+
"""Instance name."""
|
|
162
|
+
return self._name
|
|
163
|
+
|
|
164
|
+
def _search_local_root(
|
|
165
|
+
self, local_root: str | None = None, mute_warning: bool = False
|
|
166
|
+
) -> StorageSettings | None:
|
|
167
|
+
from lamindb.models import Storage
|
|
168
|
+
|
|
169
|
+
if local_root is not None:
|
|
170
|
+
local_records = Storage.objects.filter(root=local_root)
|
|
171
|
+
else:
|
|
172
|
+
# only search local managed storage locations (instance_uid=self.uid)
|
|
173
|
+
local_records = Storage.objects.filter(type="local", instance_uid=self.uid)
|
|
174
|
+
all_local_records = local_records.all()
|
|
175
|
+
try:
|
|
176
|
+
# trigger an error in case of a migration issue
|
|
177
|
+
all_local_records.first()
|
|
178
|
+
except ProgrammingError:
|
|
179
|
+
logger.error("not able to load Storage registry: please migrate")
|
|
180
|
+
return None
|
|
181
|
+
found = []
|
|
182
|
+
for record in all_local_records:
|
|
183
|
+
root_path = Path(record.root)
|
|
184
|
+
try:
|
|
185
|
+
root_path_exists = root_path.exists()
|
|
186
|
+
except PermissionError:
|
|
187
|
+
continue
|
|
188
|
+
if root_path_exists:
|
|
189
|
+
marker_path = root_path / STORAGE_UID_FILE_KEY
|
|
190
|
+
try:
|
|
191
|
+
marker_path_exists = marker_path.exists()
|
|
192
|
+
except PermissionError:
|
|
193
|
+
continue
|
|
194
|
+
if not marker_path_exists:
|
|
195
|
+
legacy_filepath = root_path / LEGACY_STORAGE_UID_FILE_KEY
|
|
196
|
+
if legacy_filepath.exists():
|
|
197
|
+
logger.warning(
|
|
198
|
+
f"found legacy marker file, renaming it from {legacy_filepath} to {marker_path}"
|
|
199
|
+
)
|
|
200
|
+
legacy_filepath.rename(marker_path)
|
|
201
|
+
else:
|
|
202
|
+
logger.warning(
|
|
203
|
+
f"local storage location '{root_path}' is corrupted, cannot find marker file with storage uid"
|
|
204
|
+
)
|
|
205
|
+
continue
|
|
206
|
+
try:
|
|
207
|
+
uid = marker_path.read_text().splitlines()[0]
|
|
208
|
+
except PermissionError:
|
|
209
|
+
logger.warning(
|
|
210
|
+
f"ignoring the following location because no permission to read it: {marker_path}"
|
|
211
|
+
)
|
|
212
|
+
continue
|
|
213
|
+
if uid == record.uid:
|
|
214
|
+
found.append(record)
|
|
215
|
+
if found:
|
|
216
|
+
if len(found) > 1:
|
|
217
|
+
found_display = "\n - ".join([f"{record.root}" for record in found])
|
|
218
|
+
logger.important(f"found locations:\n - {found_display}")
|
|
219
|
+
record = found[0]
|
|
220
|
+
logger.important(f"defaulting to local storage: {record.root}")
|
|
221
|
+
return StorageSettings(record.root, region=record.region)
|
|
222
|
+
elif not mute_warning:
|
|
223
|
+
start = LOCAL_STORAGE_MESSAGE[0].lower()
|
|
224
|
+
logger.warning(f"{start}{LOCAL_STORAGE_MESSAGE[1:]}")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def keep_artifacts_local(self) -> bool:
|
|
229
|
+
"""Default to keeping artifacts local.
|
|
230
|
+
|
|
231
|
+
Guide: :doc:`faq/keep-artifacts-local`
|
|
232
|
+
"""
|
|
233
|
+
return self._keep_artifacts_local
|
|
234
|
+
|
|
235
|
+
@keep_artifacts_local.setter
|
|
236
|
+
def keep_artifacts_local(self, value: bool):
|
|
237
|
+
if not isinstance(value, bool):
|
|
238
|
+
raise ValueError("keep_artifacts_local must be a boolean value.")
|
|
239
|
+
self._keep_artifacts_local = value
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def storage(self) -> StorageSettings:
|
|
243
|
+
"""Default storage of instance.
|
|
244
|
+
|
|
245
|
+
For a cloud instance, this is cloud storage. For a local instance, this
|
|
246
|
+
is a local directory.
|
|
247
|
+
"""
|
|
248
|
+
return self._storage # type: ignore
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def local_storage(self) -> StorageSettings:
|
|
252
|
+
"""An alternative default local storage location in the current environment.
|
|
253
|
+
|
|
254
|
+
Serves as the default storage location if :attr:`keep_artifacts_local` is enabled.
|
|
255
|
+
|
|
256
|
+
Guide: :doc:`faq/keep-artifacts-local`
|
|
257
|
+
"""
|
|
258
|
+
if not self.keep_artifacts_local:
|
|
259
|
+
raise ValueError(
|
|
260
|
+
"`keep_artifacts_local` is False, switch via: ln.setup.settings.instance.keep_artifacts_local = True"
|
|
261
|
+
)
|
|
262
|
+
if self._local_storage is None:
|
|
263
|
+
self._local_storage = self._search_local_root()
|
|
264
|
+
if self._local_storage is None:
|
|
265
|
+
raise ValueError(LOCAL_STORAGE_MESSAGE)
|
|
266
|
+
return self._local_storage
|
|
267
|
+
|
|
268
|
+
@local_storage.setter
|
|
269
|
+
def local_storage(self, local_root_host: tuple[Path | str, str]):
|
|
270
|
+
from lamindb_setup._init_instance import register_storage_in_instance
|
|
271
|
+
|
|
272
|
+
if not isinstance(local_root_host, tuple):
|
|
273
|
+
local_root = local_root_host
|
|
274
|
+
host = "unspecified-host"
|
|
275
|
+
else:
|
|
276
|
+
local_root, host = local_root_host
|
|
277
|
+
|
|
278
|
+
local_root = Path(local_root)
|
|
279
|
+
if not self.keep_artifacts_local:
|
|
280
|
+
raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
|
|
281
|
+
local_storage = self._search_local_root(
|
|
282
|
+
local_root=StorageSettings(local_root).root_as_str, mute_warning=True
|
|
283
|
+
)
|
|
284
|
+
if local_storage is not None:
|
|
285
|
+
# great, we're merely switching storage location
|
|
286
|
+
self._local_storage = local_storage
|
|
287
|
+
return None
|
|
288
|
+
local_storage = self._search_local_root(mute_warning=True)
|
|
289
|
+
if local_storage is not None:
|
|
290
|
+
if os.getenv("LAMIN_TESTING") == "true":
|
|
291
|
+
response = "y"
|
|
292
|
+
else:
|
|
293
|
+
response = input(
|
|
294
|
+
"You already configured a local storage root for this instance in this"
|
|
295
|
+
f" environment: {self.local_storage.root}\nDo you want to register another one? (y/n)"
|
|
296
|
+
)
|
|
297
|
+
if response != "y":
|
|
298
|
+
return None
|
|
299
|
+
if host == "unspecified-host":
|
|
300
|
+
logger.warning(
|
|
301
|
+
"setting local_storage with a single path is deprecated for creating storage locations"
|
|
302
|
+
)
|
|
303
|
+
logger.warning(
|
|
304
|
+
"use this instead: ln.Storage(root='/dir/our_shared_dir', host='our-server-123').save()"
|
|
305
|
+
)
|
|
306
|
+
local_root = UPath(local_root)
|
|
307
|
+
assert isinstance(local_root, LocalPathClasses)
|
|
308
|
+
tentative_storage, hub_status = init_storage(
|
|
309
|
+
local_root,
|
|
310
|
+
instance_id=self._id,
|
|
311
|
+
instance_slug=self.slug,
|
|
312
|
+
register_hub=True,
|
|
313
|
+
region=host,
|
|
314
|
+
) # type: ignore
|
|
315
|
+
if hub_status in ["hub-record-created", "hub-record-retrieved"]:
|
|
316
|
+
register_storage_in_instance(tentative_storage) # type: ignore
|
|
317
|
+
self._local_storage = tentative_storage
|
|
318
|
+
logger.important(
|
|
319
|
+
f"defaulting to local storage: {self._local_storage.root} on host {host}"
|
|
320
|
+
)
|
|
321
|
+
else:
|
|
322
|
+
logger.warning(f"could not set this local storage location: {local_root}")
|
|
323
|
+
|
|
324
|
+
@property
|
|
325
|
+
@deprecated("local_storage")
|
|
326
|
+
def storage_local(self) -> StorageSettings:
|
|
327
|
+
return self.local_storage
|
|
328
|
+
|
|
329
|
+
@storage_local.setter
|
|
330
|
+
@deprecated("local_storage")
|
|
331
|
+
def storage_local(self, local_root_host: tuple[Path | str, str]):
|
|
332
|
+
self.local_storage = local_root_host # type: ignore
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def slug(self) -> str:
|
|
336
|
+
"""Unique semantic identifier of form `"{account_handle}/{instance_name}"`."""
|
|
337
|
+
return f"{self.owner}/{self.name}"
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def git_repo(self) -> str | None:
|
|
341
|
+
"""Sync transforms with scripts in git repository.
|
|
342
|
+
|
|
343
|
+
Provide the full git repo URL.
|
|
344
|
+
"""
|
|
345
|
+
return self._git_repo
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def api_url(self) -> str | None:
|
|
349
|
+
"""URL for REST API.
|
|
350
|
+
|
|
351
|
+
Use this URL for API calls related to this instance.
|
|
352
|
+
"""
|
|
353
|
+
return self._api_url
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def available_spaces(self) -> dict | None:
|
|
357
|
+
"""Available spaces with roles for instances fine-grained permissions.
|
|
358
|
+
|
|
359
|
+
Returns a dictionary with roles as keys and lists of available spaces
|
|
360
|
+
as values if this instance has fine-grained permissions and the current user
|
|
361
|
+
is a collaborator, `None` otherwise.
|
|
362
|
+
"""
|
|
363
|
+
if self._db_permissions != "jwt":
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
from lamindb.models import Space
|
|
367
|
+
|
|
368
|
+
spaces: dict = {"admin": [], "write": [], "read": []}
|
|
369
|
+
with connection.cursor() as cur:
|
|
370
|
+
cur.execute("SELECT * FROM check_access() WHERE type = 'space'")
|
|
371
|
+
rows = cur.fetchall()
|
|
372
|
+
for row in rows:
|
|
373
|
+
spaces[row[1]].append(row[0])
|
|
374
|
+
return {
|
|
375
|
+
k: Space.filter(id__in=v).to_list() if v else [] for k, v in spaces.items()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
@property
|
|
379
|
+
def _id(self) -> UUID:
|
|
380
|
+
"""The internal instance id."""
|
|
381
|
+
return self._id_
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def uid(self) -> str:
|
|
385
|
+
"""The user-facing instance id."""
|
|
386
|
+
return instance_uid_from_uuid(self._id)
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def modules(self) -> set[str]:
|
|
390
|
+
"""The set of modules that defines the database schema.
|
|
391
|
+
|
|
392
|
+
The core schema contained in lamindb is not included in this set.
|
|
393
|
+
"""
|
|
394
|
+
if self._schema_str is None:
|
|
395
|
+
return set()
|
|
396
|
+
else:
|
|
397
|
+
return {module for module in self._schema_str.split(",") if module != ""}
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
@deprecated("modules")
|
|
401
|
+
def schema(self) -> set[str]:
|
|
402
|
+
return self.modules
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def _sqlite_file(self) -> UPath:
|
|
406
|
+
"""SQLite file."""
|
|
407
|
+
filepath = self.storage.root / ".lamindb/lamin.db"
|
|
408
|
+
return filepath
|
|
409
|
+
|
|
410
|
+
@property
|
|
411
|
+
def _sqlite_file_local(self) -> Path:
|
|
412
|
+
"""Local SQLite file."""
|
|
413
|
+
return self.storage.cloud_to_local_no_update(self._sqlite_file)
|
|
414
|
+
|
|
415
|
+
def _update_cloud_sqlite_file(self, unlock_cloud_sqlite: bool = True) -> None:
|
|
416
|
+
"""Upload the local sqlite file to the cloud file."""
|
|
417
|
+
if self._is_cloud_sqlite:
|
|
418
|
+
sqlite_file = self._sqlite_file
|
|
419
|
+
logger.warning(
|
|
420
|
+
f"updating{' & unlocking' if unlock_cloud_sqlite else ''} cloud SQLite "
|
|
421
|
+
f"'{sqlite_file}' of instance"
|
|
422
|
+
f" '{self.slug}'"
|
|
423
|
+
)
|
|
424
|
+
cache_file = self.storage.cloud_to_local_no_update(sqlite_file)
|
|
425
|
+
sqlite_file.upload_from(cache_file, print_progress=True) # type: ignore
|
|
426
|
+
cloud_mtime = sqlite_file.modified.timestamp() # type: ignore
|
|
427
|
+
# this seems to work even if there is an open connection
|
|
428
|
+
# to the cache file
|
|
429
|
+
os.utime(cache_file, times=(cloud_mtime, cloud_mtime))
|
|
430
|
+
if unlock_cloud_sqlite:
|
|
431
|
+
self._cloud_sqlite_locker.unlock()
|
|
432
|
+
|
|
433
|
+
def _update_local_sqlite_file(self, lock_cloud_sqlite: bool = True) -> None:
|
|
434
|
+
"""Download the cloud sqlite file if it is newer than local."""
|
|
435
|
+
if self._is_cloud_sqlite:
|
|
436
|
+
logger.warning(
|
|
437
|
+
"updating local SQLite & locking cloud SQLite (sync back & unlock:"
|
|
438
|
+
" lamin disconnect)"
|
|
439
|
+
)
|
|
440
|
+
if lock_cloud_sqlite:
|
|
441
|
+
self._cloud_sqlite_locker.lock()
|
|
442
|
+
self._check_sqlite_lock()
|
|
443
|
+
sqlite_file = self._sqlite_file
|
|
444
|
+
cache_file = self.storage.cloud_to_local_no_update(sqlite_file)
|
|
445
|
+
sqlite_file.synchronize_to(cache_file, print_progress=True) # type: ignore
|
|
446
|
+
|
|
447
|
+
def _check_sqlite_lock(self):
|
|
448
|
+
if not self._cloud_sqlite_locker.has_lock:
|
|
449
|
+
locked_by = self._cloud_sqlite_locker._locked_by
|
|
450
|
+
lock_msg = "Cannot load the instance, it is locked by "
|
|
451
|
+
user_info = call_with_fallback(
|
|
452
|
+
select_account_handle_name_by_lnid,
|
|
453
|
+
lnid=locked_by,
|
|
454
|
+
)
|
|
455
|
+
if user_info is None:
|
|
456
|
+
lock_msg += f"uid: '{locked_by}'."
|
|
457
|
+
else:
|
|
458
|
+
lock_msg += (
|
|
459
|
+
f"'{user_info['handle']}' (uid: '{locked_by}', name:"
|
|
460
|
+
f" '{user_info['name']}')."
|
|
461
|
+
)
|
|
462
|
+
lock_msg += (
|
|
463
|
+
" The instance will be automatically unlocked after"
|
|
464
|
+
f" {int(EXPIRATION_TIME/3600/24)}d of no activity."
|
|
465
|
+
)
|
|
466
|
+
raise InstanceLockedException(lock_msg)
|
|
467
|
+
|
|
468
|
+
@property
|
|
469
|
+
def db(self) -> str:
|
|
470
|
+
"""Database connection string (URI)."""
|
|
471
|
+
if "LAMINDB_DJANGO_DATABASE_URL" in os.environ:
|
|
472
|
+
logger.warning(
|
|
473
|
+
"LAMINDB_DJANGO_DATABASE_URL env variable "
|
|
474
|
+
f"is set to {os.environ['LAMINDB_DJANGO_DATABASE_URL']}. "
|
|
475
|
+
"It overwrites all db connections and is used instead of `instance.db`."
|
|
476
|
+
)
|
|
477
|
+
if self._db is None:
|
|
478
|
+
from .django import IS_SETUP
|
|
479
|
+
|
|
480
|
+
if self._storage is None and self.slug == "none/none":
|
|
481
|
+
return "sqlite:///:memory:"
|
|
482
|
+
# here, we want the updated sqlite file
|
|
483
|
+
# hence, we don't use self._sqlite_file_local()
|
|
484
|
+
# error_no_origin=False because on instance init
|
|
485
|
+
# the sqlite file is not yet in the cloud
|
|
486
|
+
sqlite_filepath = self.storage.cloud_to_local(
|
|
487
|
+
self._sqlite_file, error_no_origin=False
|
|
488
|
+
)
|
|
489
|
+
return f"sqlite:///{sqlite_filepath.as_posix()}"
|
|
490
|
+
else:
|
|
491
|
+
return self._db
|
|
492
|
+
|
|
493
|
+
@property
|
|
494
|
+
def dialect(self) -> Literal["sqlite", "postgresql"]:
|
|
495
|
+
"""SQL dialect."""
|
|
496
|
+
if self._db is None or self._db.startswith("sqlite://"):
|
|
497
|
+
return "sqlite"
|
|
498
|
+
else:
|
|
499
|
+
assert self._db.startswith("postgresql"), f"Unexpected DB value: {self._db}"
|
|
500
|
+
return "postgresql"
|
|
501
|
+
|
|
502
|
+
@property
|
|
503
|
+
def _is_cloud_sqlite(self) -> bool:
|
|
504
|
+
# can we make this a private property, Sergei?
|
|
505
|
+
# as it's not relevant to the user
|
|
506
|
+
"""Is this a cloud instance with sqlite db."""
|
|
507
|
+
return self.dialect == "sqlite" and self.storage.type_is_cloud
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def _cloud_sqlite_locker(self):
|
|
511
|
+
# avoid circular import
|
|
512
|
+
from .cloud_sqlite_locker import empty_locker, get_locker
|
|
513
|
+
|
|
514
|
+
if self._is_cloud_sqlite:
|
|
515
|
+
try:
|
|
516
|
+
# if _locker_user is None then settings.user is used
|
|
517
|
+
return get_locker(self, self._locker_user)
|
|
518
|
+
except PermissionError:
|
|
519
|
+
logger.warning("read-only access - did not access locker")
|
|
520
|
+
return empty_locker
|
|
521
|
+
else:
|
|
522
|
+
return empty_locker
|
|
523
|
+
|
|
524
|
+
@property
|
|
525
|
+
def is_remote(self) -> bool:
|
|
526
|
+
"""Boolean indicating if an instance has no local component."""
|
|
527
|
+
return check_is_instance_remote(self.storage.root_as_str, self.db)
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def is_on_hub(self) -> bool:
|
|
531
|
+
"""Is this instance on the hub?
|
|
532
|
+
|
|
533
|
+
Can only reliably establish if user has access to the instance. Will
|
|
534
|
+
return `False` in case the instance isn't found.
|
|
535
|
+
"""
|
|
536
|
+
if self._is_on_hub is None:
|
|
537
|
+
from ._hub_client import call_with_fallback_auth
|
|
538
|
+
from ._hub_crud import select_instance_by_id
|
|
539
|
+
from ._settings import settings
|
|
540
|
+
|
|
541
|
+
if settings.user.handle != "anonymous":
|
|
542
|
+
response = call_with_fallback_auth(
|
|
543
|
+
select_instance_by_id, instance_id=self._id.hex
|
|
544
|
+
)
|
|
545
|
+
else:
|
|
546
|
+
response = call_with_fallback(
|
|
547
|
+
select_instance_by_id, instance_id=self._id.hex
|
|
548
|
+
)
|
|
549
|
+
logger.warning("calling anonymously, will miss private instances")
|
|
550
|
+
if response is None:
|
|
551
|
+
self._is_on_hub = False
|
|
552
|
+
else:
|
|
553
|
+
self._is_on_hub = True
|
|
554
|
+
return self._is_on_hub
|
|
555
|
+
|
|
556
|
+
def _get_settings_file(self) -> Path:
|
|
557
|
+
return instance_settings_file(self.name, self.owner)
|
|
558
|
+
|
|
559
|
+
def _persist(self, write_to_disk: bool = True) -> None:
|
|
560
|
+
"""Set these instance settings as the current instance.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
write_to_disk: Save these instance settings to disk and
|
|
564
|
+
overwrite the current instance settings file.
|
|
565
|
+
"""
|
|
566
|
+
if write_to_disk and self.slug != "none/none":
|
|
567
|
+
assert self.name is not None
|
|
568
|
+
filepath = self._get_settings_file()
|
|
569
|
+
# persist under filepath for later reference
|
|
570
|
+
save_instance_settings(self, filepath)
|
|
571
|
+
# persist under current file for auto load
|
|
572
|
+
shutil.copy2(filepath, current_instance_settings_file())
|
|
573
|
+
# persist under settings class for same session reference
|
|
574
|
+
# need to import here to avoid circular import
|
|
575
|
+
from ._settings import settings
|
|
576
|
+
|
|
577
|
+
settings._instance_settings = self
|
|
578
|
+
|
|
579
|
+
def _init_db(self):
|
|
580
|
+
from lamindb_setup._check_setup import disable_auto_connect
|
|
581
|
+
|
|
582
|
+
from .django import setup_django
|
|
583
|
+
|
|
584
|
+
disable_auto_connect(setup_django)(self, init=True)
|
|
585
|
+
|
|
586
|
+
def _load_db(self) -> tuple[bool, str]:
|
|
587
|
+
# Is the database available and initialized as LaminDB?
|
|
588
|
+
# returns a tuple of status code and message
|
|
589
|
+
if self.dialect == "sqlite" and not self._sqlite_file.exists():
|
|
590
|
+
legacy_file = self.storage.key_to_filepath(f"{self._id.hex}.lndb")
|
|
591
|
+
if legacy_file.exists():
|
|
592
|
+
logger.warning(
|
|
593
|
+
f"The SQLite file is being renamed from {legacy_file} to {self._sqlite_file}"
|
|
594
|
+
)
|
|
595
|
+
legacy_file.rename(self._sqlite_file)
|
|
596
|
+
else:
|
|
597
|
+
return False, f"SQLite file {self._sqlite_file} does not exist"
|
|
598
|
+
# we need the local sqlite to setup django
|
|
599
|
+
self._update_local_sqlite_file()
|
|
600
|
+
# setting up django also performs a check for migrations & prints them
|
|
601
|
+
# as warnings
|
|
602
|
+
# this should fail, e.g., if the db is not reachable
|
|
603
|
+
from lamindb_setup._check_setup import disable_auto_connect
|
|
604
|
+
|
|
605
|
+
from .django import setup_django
|
|
606
|
+
|
|
607
|
+
disable_auto_connect(setup_django)(self)
|
|
608
|
+
|
|
609
|
+
return True, ""
|