lamindb_setup 0.77.3__py2.py3-none-any.whl → 0.77.5__py2.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 +1 -1
- lamindb_setup/_cache.py +34 -34
- lamindb_setup/_check.py +7 -7
- lamindb_setup/_check_setup.py +79 -79
- lamindb_setup/_close.py +35 -35
- lamindb_setup/_connect_instance.py +431 -444
- lamindb_setup/_delete.py +141 -139
- lamindb_setup/_django.py +41 -41
- lamindb_setup/_entry_points.py +22 -22
- lamindb_setup/_exportdb.py +68 -68
- lamindb_setup/_importdb.py +50 -50
- lamindb_setup/_init_instance.py +417 -374
- lamindb_setup/_migrate.py +239 -239
- lamindb_setup/_register_instance.py +36 -36
- lamindb_setup/_schema.py +27 -27
- lamindb_setup/_schema_metadata.py +411 -411
- lamindb_setup/_set_managed_storage.py +55 -55
- lamindb_setup/_setup_user.py +137 -137
- lamindb_setup/_silence_loggers.py +44 -44
- lamindb_setup/core/__init__.py +21 -21
- lamindb_setup/core/_aws_credentials.py +151 -151
- lamindb_setup/core/_aws_storage.py +48 -48
- lamindb_setup/core/_deprecated.py +55 -55
- lamindb_setup/core/_docs.py +14 -14
- lamindb_setup/core/_hub_core.py +611 -590
- lamindb_setup/core/_hub_crud.py +211 -211
- lamindb_setup/core/_hub_utils.py +109 -109
- lamindb_setup/core/_private_django_api.py +88 -88
- lamindb_setup/core/_settings.py +138 -138
- lamindb_setup/core/_settings_instance.py +480 -467
- lamindb_setup/core/_settings_load.py +105 -105
- lamindb_setup/core/_settings_save.py +81 -81
- lamindb_setup/core/_settings_storage.py +412 -405
- lamindb_setup/core/_settings_store.py +75 -75
- lamindb_setup/core/_settings_user.py +53 -53
- lamindb_setup/core/_setup_bionty_sources.py +101 -101
- lamindb_setup/core/cloud_sqlite_locker.py +237 -232
- lamindb_setup/core/django.py +114 -114
- lamindb_setup/core/exceptions.py +12 -12
- lamindb_setup/core/hashing.py +114 -114
- lamindb_setup/core/types.py +19 -19
- lamindb_setup/core/upath.py +779 -779
- {lamindb_setup-0.77.3.dist-info → lamindb_setup-0.77.5.dist-info}/METADATA +1 -1
- lamindb_setup-0.77.5.dist-info/RECORD +47 -0
- {lamindb_setup-0.77.3.dist-info → lamindb_setup-0.77.5.dist-info}/WHEEL +1 -1
- lamindb_setup-0.77.3.dist-info/RECORD +0 -47
- {lamindb_setup-0.77.3.dist-info → lamindb_setup-0.77.5.dist-info}/LICENSE +0 -0
|
@@ -1,467 +1,480 @@
|
|
|
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.utils import ProgrammingError
|
|
9
|
-
from lamin_utils import logger
|
|
10
|
-
|
|
11
|
-
from ._hub_client import call_with_fallback
|
|
12
|
-
from ._hub_crud import select_account_handle_name_by_lnid
|
|
13
|
-
from ._hub_utils import LaminDsn, LaminDsnModel
|
|
14
|
-
from ._settings_save import save_instance_settings
|
|
15
|
-
from ._settings_storage import StorageSettings, init_storage, mark_storage_root
|
|
16
|
-
from ._settings_store import current_instance_settings_file, instance_settings_file
|
|
17
|
-
from .cloud_sqlite_locker import (
|
|
18
|
-
EXPIRATION_TIME,
|
|
19
|
-
InstanceLockedException,
|
|
20
|
-
)
|
|
21
|
-
from .upath import LocalPathClasses, UPath
|
|
22
|
-
|
|
23
|
-
if TYPE_CHECKING:
|
|
24
|
-
from uuid import UUID
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if "@
|
|
36
|
-
return True
|
|
37
|
-
if "@
|
|
38
|
-
return True
|
|
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
|
-
self.
|
|
64
|
-
self.
|
|
65
|
-
|
|
66
|
-
self.
|
|
67
|
-
self.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
self.
|
|
71
|
-
self.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
@property
|
|
107
|
-
def
|
|
108
|
-
"""Instance
|
|
109
|
-
return self.
|
|
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
|
-
raise ValueError()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
if
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
storage_local
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
@property
|
|
244
|
-
def
|
|
245
|
-
"""
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
"""
|
|
254
|
-
return self.
|
|
255
|
-
|
|
256
|
-
@property
|
|
257
|
-
def
|
|
258
|
-
"""The
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
@property
|
|
277
|
-
def
|
|
278
|
-
"""
|
|
279
|
-
return self.storage.
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
else:
|
|
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
|
-
self.
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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.utils import ProgrammingError
|
|
9
|
+
from lamin_utils import logger
|
|
10
|
+
|
|
11
|
+
from ._hub_client import call_with_fallback
|
|
12
|
+
from ._hub_crud import select_account_handle_name_by_lnid
|
|
13
|
+
from ._hub_utils import LaminDsn, LaminDsnModel
|
|
14
|
+
from ._settings_save import save_instance_settings
|
|
15
|
+
from ._settings_storage import StorageSettings, init_storage, mark_storage_root
|
|
16
|
+
from ._settings_store import current_instance_settings_file, instance_settings_file
|
|
17
|
+
from .cloud_sqlite_locker import (
|
|
18
|
+
EXPIRATION_TIME,
|
|
19
|
+
InstanceLockedException,
|
|
20
|
+
)
|
|
21
|
+
from .upath import LocalPathClasses, UPath
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from uuid import UUID
|
|
25
|
+
|
|
26
|
+
from ._settings_user import UserSettings
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def sanitize_git_repo_url(repo_url: str) -> str:
|
|
30
|
+
assert repo_url.startswith("https://")
|
|
31
|
+
return repo_url.replace(".git", "")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_local_db_url(db_url: str) -> bool:
|
|
35
|
+
if "@localhost:" in db_url:
|
|
36
|
+
return True
|
|
37
|
+
if "@0.0.0.0:" in db_url:
|
|
38
|
+
return True
|
|
39
|
+
if "@127.0.0.1" in db_url:
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InstanceSettings:
|
|
45
|
+
"""Instance settings."""
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
id: UUID, # instance id/uuid
|
|
50
|
+
owner: str, # owner handle
|
|
51
|
+
name: str, # instance name
|
|
52
|
+
storage: StorageSettings, # storage location
|
|
53
|
+
keep_artifacts_local: bool = False, # default to local storage
|
|
54
|
+
uid: str | None = None, # instance uid/lnid
|
|
55
|
+
db: str | None = None, # DB URI
|
|
56
|
+
schema: str | None = None, # comma-separated string of schema names
|
|
57
|
+
git_repo: str | None = None, # a git repo URL
|
|
58
|
+
is_on_hub: bool | None = None, # initialized from hub
|
|
59
|
+
_locker_user: UserSettings | None = None, # user to lock for if cloud sqlite
|
|
60
|
+
):
|
|
61
|
+
from ._hub_utils import validate_db_arg
|
|
62
|
+
|
|
63
|
+
self._id_: UUID = id
|
|
64
|
+
self._owner: str = owner
|
|
65
|
+
self._name: str = name
|
|
66
|
+
self._uid: str | None = uid
|
|
67
|
+
self._storage: StorageSettings = storage
|
|
68
|
+
validate_db_arg(db)
|
|
69
|
+
self._db: str | None = db
|
|
70
|
+
self._schema_str: str | None = schema
|
|
71
|
+
self._git_repo = None if git_repo is None else sanitize_git_repo_url(git_repo)
|
|
72
|
+
# local storage
|
|
73
|
+
self._keep_artifacts_local = keep_artifacts_local
|
|
74
|
+
self._storage_local: StorageSettings | None = None
|
|
75
|
+
self._is_on_hub = is_on_hub
|
|
76
|
+
# if None then settings.user is used
|
|
77
|
+
self._locker_user = _locker_user
|
|
78
|
+
|
|
79
|
+
def __repr__(self):
|
|
80
|
+
"""Rich string representation."""
|
|
81
|
+
representation = f"Current instance: {self.slug}"
|
|
82
|
+
attrs = ["owner", "name", "storage", "db", "schema", "git_repo"]
|
|
83
|
+
for attr in attrs:
|
|
84
|
+
value = getattr(self, attr)
|
|
85
|
+
if attr == "storage":
|
|
86
|
+
representation += f"\n- storage root: {value.root_as_str}"
|
|
87
|
+
representation += f"\n- storage region: {value.region}"
|
|
88
|
+
elif attr == "db":
|
|
89
|
+
if self.dialect != "sqlite":
|
|
90
|
+
model = LaminDsnModel(db=value)
|
|
91
|
+
db_print = LaminDsn.build(
|
|
92
|
+
scheme=model.db.scheme,
|
|
93
|
+
user=model.db.user,
|
|
94
|
+
password="***",
|
|
95
|
+
host="***",
|
|
96
|
+
port=model.db.port,
|
|
97
|
+
database=model.db.database,
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
db_print = value
|
|
101
|
+
representation += f"\n- {attr}: {db_print}"
|
|
102
|
+
else:
|
|
103
|
+
representation += f"\n- {attr}: {value}"
|
|
104
|
+
return representation
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def owner(self) -> str:
|
|
108
|
+
"""Instance owner. A user or organization account handle."""
|
|
109
|
+
return self._owner
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def name(self) -> str:
|
|
113
|
+
"""Instance name."""
|
|
114
|
+
return self._name
|
|
115
|
+
|
|
116
|
+
def _search_local_root(
|
|
117
|
+
self, local_root: str | None = None, mute_warning: bool = False
|
|
118
|
+
) -> StorageSettings | None:
|
|
119
|
+
from lnschema_core.models import Storage
|
|
120
|
+
|
|
121
|
+
if local_root is not None:
|
|
122
|
+
local_records = Storage.objects.filter(root=local_root)
|
|
123
|
+
else:
|
|
124
|
+
# only search local managed storage locations (instance_uid=self.uid)
|
|
125
|
+
local_records = Storage.objects.filter(type="local", instance_uid=self.uid)
|
|
126
|
+
all_local_records = local_records.all()
|
|
127
|
+
try:
|
|
128
|
+
# trigger an error in case of a migration issue
|
|
129
|
+
all_local_records.first()
|
|
130
|
+
except ProgrammingError:
|
|
131
|
+
logger.error("not able to load Storage registry: please migrate")
|
|
132
|
+
return None
|
|
133
|
+
found = False
|
|
134
|
+
for record in all_local_records:
|
|
135
|
+
root_path = Path(record.root)
|
|
136
|
+
if root_path.exists():
|
|
137
|
+
marker_path = root_path / ".lamindb/_is_initialized"
|
|
138
|
+
if marker_path.exists():
|
|
139
|
+
try:
|
|
140
|
+
uid = marker_path.read_text()
|
|
141
|
+
except PermissionError:
|
|
142
|
+
logger.warning(
|
|
143
|
+
f"ignoring the following location because no permission to read it: {marker_path}"
|
|
144
|
+
)
|
|
145
|
+
continue
|
|
146
|
+
if uid == record.uid:
|
|
147
|
+
found = True
|
|
148
|
+
break
|
|
149
|
+
elif uid == "":
|
|
150
|
+
try:
|
|
151
|
+
# legacy instance that was not yet marked properly
|
|
152
|
+
mark_storage_root(record.root, record.uid)
|
|
153
|
+
except PermissionError:
|
|
154
|
+
logger.warning(
|
|
155
|
+
f"ignoring the following location because no permission to write to it: {marker_path}"
|
|
156
|
+
)
|
|
157
|
+
continue
|
|
158
|
+
else:
|
|
159
|
+
continue
|
|
160
|
+
else:
|
|
161
|
+
# legacy instance that was not yet marked at all
|
|
162
|
+
mark_storage_root(record.root, record.uid)
|
|
163
|
+
break
|
|
164
|
+
if found:
|
|
165
|
+
return StorageSettings(record.root)
|
|
166
|
+
elif not mute_warning:
|
|
167
|
+
logger.warning(
|
|
168
|
+
f"none of the registered local storage locations were found in your environment: {local_records}"
|
|
169
|
+
)
|
|
170
|
+
logger.important(
|
|
171
|
+
"please register a new local storage location via `ln.settings.storage_local = local_root_path` and re-load/connect the instance"
|
|
172
|
+
)
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def keep_artifacts_local(self) -> bool:
|
|
177
|
+
"""Default to keeping artifacts local.
|
|
178
|
+
|
|
179
|
+
Enable this optional setting for cloud instances on lamin.ai.
|
|
180
|
+
|
|
181
|
+
Guide: :doc:`faq/keep-artifacts-local`
|
|
182
|
+
"""
|
|
183
|
+
return self._keep_artifacts_local
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def storage(self) -> StorageSettings:
|
|
187
|
+
"""Default storage.
|
|
188
|
+
|
|
189
|
+
For a cloud instance, this is cloud storage. For a local instance, this
|
|
190
|
+
is a local directory.
|
|
191
|
+
"""
|
|
192
|
+
return self._storage
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def storage_local(self) -> StorageSettings:
|
|
196
|
+
"""An additional local default storage.
|
|
197
|
+
|
|
198
|
+
Is only available if :attr:`keep_artifacts_local` is enabled.
|
|
199
|
+
|
|
200
|
+
Guide: :doc:`faq/keep-artifacts-local`
|
|
201
|
+
"""
|
|
202
|
+
if not self._keep_artifacts_local:
|
|
203
|
+
raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
|
|
204
|
+
if self._storage_local is None:
|
|
205
|
+
self._storage_local = self._search_local_root()
|
|
206
|
+
if self._storage_local is None:
|
|
207
|
+
# raise an error, there was a warning just before in search_local_root
|
|
208
|
+
raise ValueError()
|
|
209
|
+
return self._storage_local
|
|
210
|
+
|
|
211
|
+
@storage_local.setter
|
|
212
|
+
def storage_local(self, local_root: Path | str):
|
|
213
|
+
from lamindb_setup._init_instance import register_storage_in_instance
|
|
214
|
+
|
|
215
|
+
local_root = Path(local_root)
|
|
216
|
+
if not self._keep_artifacts_local:
|
|
217
|
+
raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
|
|
218
|
+
storage_local = self._search_local_root(
|
|
219
|
+
local_root=StorageSettings(local_root).root_as_str, mute_warning=True
|
|
220
|
+
)
|
|
221
|
+
if storage_local is not None:
|
|
222
|
+
# great, we're merely switching storage location
|
|
223
|
+
self._storage_local = storage_local
|
|
224
|
+
logger.important(f"defaulting to local storage: {storage_local.root}")
|
|
225
|
+
return None
|
|
226
|
+
storage_local = self._search_local_root(mute_warning=True)
|
|
227
|
+
if storage_local is not None:
|
|
228
|
+
if os.getenv("LAMIN_TESTING") == "true":
|
|
229
|
+
response = "y"
|
|
230
|
+
else:
|
|
231
|
+
response = input(
|
|
232
|
+
"You already configured a local storage root for this instance in this"
|
|
233
|
+
f" environment: {self.storage_local.root}\nDo you want to register another one? (y/n)"
|
|
234
|
+
)
|
|
235
|
+
if response != "y":
|
|
236
|
+
return None
|
|
237
|
+
local_root = UPath(local_root)
|
|
238
|
+
assert isinstance(local_root, LocalPathClasses)
|
|
239
|
+
self._storage_local, _ = init_storage(local_root, self._id, register_hub=True) # type: ignore
|
|
240
|
+
register_storage_in_instance(self._storage_local) # type: ignore
|
|
241
|
+
logger.important(f"defaulting to local storage: {self._storage_local.root}")
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def slug(self) -> str:
|
|
245
|
+
"""Unique semantic identifier of form `"{account_handle}/{instance_name}"`."""
|
|
246
|
+
return f"{self.owner}/{self.name}"
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def git_repo(self) -> str | None:
|
|
250
|
+
"""Sync transforms with scripts in git repository.
|
|
251
|
+
|
|
252
|
+
Provide the full git repo URL.
|
|
253
|
+
"""
|
|
254
|
+
return self._git_repo
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def _id(self) -> UUID:
|
|
258
|
+
"""The internal instance id."""
|
|
259
|
+
return self._id_
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def uid(self) -> str:
|
|
263
|
+
"""The user-facing instance id."""
|
|
264
|
+
from .hashing import hash_and_encode_as_b62
|
|
265
|
+
|
|
266
|
+
return hash_and_encode_as_b62(self._id.hex)[:12]
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def schema(self) -> set[str]:
|
|
270
|
+
"""Schema modules in addition to core schema."""
|
|
271
|
+
if self._schema_str is None:
|
|
272
|
+
return {} # type: ignore
|
|
273
|
+
else:
|
|
274
|
+
return {schema for schema in self._schema_str.split(",") if schema != ""}
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def _sqlite_file(self) -> UPath:
|
|
278
|
+
"""SQLite file."""
|
|
279
|
+
return self.storage.key_to_filepath(f"{self._id.hex}.lndb")
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def _sqlite_file_local(self) -> Path:
|
|
283
|
+
"""Local SQLite file."""
|
|
284
|
+
return self.storage.cloud_to_local_no_update(self._sqlite_file)
|
|
285
|
+
|
|
286
|
+
def _update_cloud_sqlite_file(self, unlock_cloud_sqlite: bool = True) -> None:
|
|
287
|
+
"""Upload the local sqlite file to the cloud file."""
|
|
288
|
+
if self._is_cloud_sqlite:
|
|
289
|
+
sqlite_file = self._sqlite_file
|
|
290
|
+
logger.warning(
|
|
291
|
+
f"updating{' & unlocking' if unlock_cloud_sqlite else ''} cloud SQLite "
|
|
292
|
+
f"'{sqlite_file}' of instance"
|
|
293
|
+
f" '{self.slug}'"
|
|
294
|
+
)
|
|
295
|
+
cache_file = self.storage.cloud_to_local_no_update(sqlite_file)
|
|
296
|
+
sqlite_file.upload_from(cache_file, print_progress=True) # type: ignore
|
|
297
|
+
cloud_mtime = sqlite_file.modified.timestamp() # type: ignore
|
|
298
|
+
# this seems to work even if there is an open connection
|
|
299
|
+
# to the cache file
|
|
300
|
+
os.utime(cache_file, times=(cloud_mtime, cloud_mtime))
|
|
301
|
+
if unlock_cloud_sqlite:
|
|
302
|
+
self._cloud_sqlite_locker.unlock()
|
|
303
|
+
|
|
304
|
+
def _update_local_sqlite_file(self, lock_cloud_sqlite: bool = True) -> None:
|
|
305
|
+
"""Download the cloud sqlite file if it is newer than local."""
|
|
306
|
+
if self._is_cloud_sqlite:
|
|
307
|
+
logger.warning(
|
|
308
|
+
"updating local SQLite & locking cloud SQLite (sync back & unlock:"
|
|
309
|
+
" lamin load --unload)"
|
|
310
|
+
)
|
|
311
|
+
if lock_cloud_sqlite:
|
|
312
|
+
self._cloud_sqlite_locker.lock()
|
|
313
|
+
self._check_sqlite_lock()
|
|
314
|
+
sqlite_file = self._sqlite_file
|
|
315
|
+
cache_file = self.storage.cloud_to_local_no_update(sqlite_file)
|
|
316
|
+
sqlite_file.synchronize(cache_file, print_progress=True) # type: ignore
|
|
317
|
+
|
|
318
|
+
def _check_sqlite_lock(self):
|
|
319
|
+
if not self._cloud_sqlite_locker.has_lock:
|
|
320
|
+
locked_by = self._cloud_sqlite_locker._locked_by
|
|
321
|
+
lock_msg = "Cannot load the instance, it is locked by "
|
|
322
|
+
user_info = call_with_fallback(
|
|
323
|
+
select_account_handle_name_by_lnid,
|
|
324
|
+
lnid=locked_by,
|
|
325
|
+
)
|
|
326
|
+
if user_info is None:
|
|
327
|
+
lock_msg += f"uid: '{locked_by}'."
|
|
328
|
+
else:
|
|
329
|
+
lock_msg += (
|
|
330
|
+
f"'{user_info['handle']}' (uid: '{locked_by}', name:"
|
|
331
|
+
f" '{user_info['name']}')."
|
|
332
|
+
)
|
|
333
|
+
lock_msg += (
|
|
334
|
+
" The instance will be automatically unlocked after"
|
|
335
|
+
f" {int(EXPIRATION_TIME/3600/24)}d of no activity."
|
|
336
|
+
)
|
|
337
|
+
raise InstanceLockedException(lock_msg)
|
|
338
|
+
|
|
339
|
+
@property
|
|
340
|
+
def db(self) -> str:
|
|
341
|
+
"""Database connection string (URI)."""
|
|
342
|
+
if "LAMINDB_DJANGO_DATABASE_URL" in os.environ:
|
|
343
|
+
logger.warning(
|
|
344
|
+
"LAMINDB_DJANGO_DATABASE_URL env variable "
|
|
345
|
+
f"is set to {os.environ['LAMINDB_DJANGO_DATABASE_URL']}. "
|
|
346
|
+
"It overwrites all db connections and is used instead of `instance.db`."
|
|
347
|
+
)
|
|
348
|
+
if self._db is None:
|
|
349
|
+
# here, we want the updated sqlite file
|
|
350
|
+
# hence, we don't use self._sqlite_file_local()
|
|
351
|
+
# error_no_origin=False because on instance init
|
|
352
|
+
# the sqlite file is not yet in the cloud
|
|
353
|
+
sqlite_filepath = self.storage.cloud_to_local(
|
|
354
|
+
self._sqlite_file, error_no_origin=False
|
|
355
|
+
)
|
|
356
|
+
return f"sqlite:///{sqlite_filepath}"
|
|
357
|
+
else:
|
|
358
|
+
return self._db
|
|
359
|
+
|
|
360
|
+
@property
|
|
361
|
+
def dialect(self) -> Literal["sqlite", "postgresql"]:
|
|
362
|
+
"""SQL dialect."""
|
|
363
|
+
if self._db is None or self._db.startswith("sqlite://"):
|
|
364
|
+
return "sqlite"
|
|
365
|
+
else:
|
|
366
|
+
assert self._db.startswith("postgresql"), f"Unexpected DB value: {self._db}"
|
|
367
|
+
return "postgresql"
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def _is_cloud_sqlite(self) -> bool:
|
|
371
|
+
# can we make this a private property, Sergei?
|
|
372
|
+
# as it's not relevant to the user
|
|
373
|
+
"""Is this a cloud instance with sqlite db."""
|
|
374
|
+
return self.dialect == "sqlite" and self.storage.type_is_cloud
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def _cloud_sqlite_locker(self):
|
|
378
|
+
# avoid circular import
|
|
379
|
+
from .cloud_sqlite_locker import empty_locker, get_locker
|
|
380
|
+
|
|
381
|
+
if self._is_cloud_sqlite:
|
|
382
|
+
try:
|
|
383
|
+
# if _locker_user is None then settings.user is used
|
|
384
|
+
return get_locker(self, self._locker_user)
|
|
385
|
+
except PermissionError:
|
|
386
|
+
logger.warning("read-only access - did not access locker")
|
|
387
|
+
return empty_locker
|
|
388
|
+
else:
|
|
389
|
+
return empty_locker
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def is_remote(self) -> bool:
|
|
393
|
+
"""Boolean indicating if an instance has no local component."""
|
|
394
|
+
if not self.storage.type_is_cloud:
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
if self.dialect == "postgresql":
|
|
398
|
+
if is_local_db_url(self.db):
|
|
399
|
+
return False
|
|
400
|
+
# returns True for cloud SQLite
|
|
401
|
+
# and remote postgres
|
|
402
|
+
return True
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def is_on_hub(self) -> bool:
|
|
406
|
+
"""Is this instance on the hub?
|
|
407
|
+
|
|
408
|
+
Can only reliably establish if user has access to the instance. Will
|
|
409
|
+
return `False` in case the instance isn't found.
|
|
410
|
+
"""
|
|
411
|
+
if self._is_on_hub is None:
|
|
412
|
+
from ._hub_client import call_with_fallback_auth
|
|
413
|
+
from ._hub_crud import select_instance_by_id
|
|
414
|
+
from ._settings import settings
|
|
415
|
+
|
|
416
|
+
if settings.user.handle != "anonymous":
|
|
417
|
+
response = call_with_fallback_auth(
|
|
418
|
+
select_instance_by_id, instance_id=self._id.hex
|
|
419
|
+
)
|
|
420
|
+
else:
|
|
421
|
+
response = call_with_fallback(
|
|
422
|
+
select_instance_by_id, instance_id=self._id.hex
|
|
423
|
+
)
|
|
424
|
+
logger.warning("calling anonymously, will miss private instances")
|
|
425
|
+
if response is None:
|
|
426
|
+
self._is_on_hub = False
|
|
427
|
+
else:
|
|
428
|
+
self._is_on_hub = True
|
|
429
|
+
return self._is_on_hub
|
|
430
|
+
|
|
431
|
+
def _get_settings_file(self) -> Path:
|
|
432
|
+
return instance_settings_file(self.name, self.owner)
|
|
433
|
+
|
|
434
|
+
def _persist(self, write_to_disk: bool = True) -> None:
|
|
435
|
+
"""Set these instance settings as the current instance.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
write_to_disk: Save these instance settings to disk and
|
|
439
|
+
overwrite the current instance settings file.
|
|
440
|
+
"""
|
|
441
|
+
if write_to_disk:
|
|
442
|
+
assert self.name is not None
|
|
443
|
+
filepath = self._get_settings_file()
|
|
444
|
+
# persist under filepath for later reference
|
|
445
|
+
save_instance_settings(self, filepath)
|
|
446
|
+
# persist under current file for auto load
|
|
447
|
+
shutil.copy2(filepath, current_instance_settings_file())
|
|
448
|
+
# persist under settings class for same session reference
|
|
449
|
+
# need to import here to avoid circular import
|
|
450
|
+
from ._settings import settings
|
|
451
|
+
|
|
452
|
+
settings._instance_settings = self
|
|
453
|
+
|
|
454
|
+
def _init_db(self):
|
|
455
|
+
from .django import setup_django
|
|
456
|
+
|
|
457
|
+
setup_django(self, init=True)
|
|
458
|
+
|
|
459
|
+
def _load_db(self) -> tuple[bool, str]:
|
|
460
|
+
# Is the database available and initialized as LaminDB?
|
|
461
|
+
# returns a tuple of status code and message
|
|
462
|
+
if self.dialect == "sqlite" and not self._sqlite_file.exists():
|
|
463
|
+
legacy_file = self.storage.key_to_filepath(f"{self.name}.lndb")
|
|
464
|
+
if legacy_file.exists():
|
|
465
|
+
raise RuntimeError(
|
|
466
|
+
"The SQLite file has been renamed!\nPlease rename your SQLite file"
|
|
467
|
+
f" {legacy_file} to {self._sqlite_file}"
|
|
468
|
+
)
|
|
469
|
+
return False, f"SQLite file {self._sqlite_file} does not exist"
|
|
470
|
+
from lamindb_setup import settings # to check user
|
|
471
|
+
|
|
472
|
+
from .django import setup_django
|
|
473
|
+
|
|
474
|
+
# we need the local sqlite to setup django
|
|
475
|
+
self._update_local_sqlite_file(lock_cloud_sqlite=self._is_cloud_sqlite)
|
|
476
|
+
# setting up django also performs a check for migrations & prints them
|
|
477
|
+
# as warnings
|
|
478
|
+
# this should fail, e.g., if the db is not reachable
|
|
479
|
+
setup_django(self)
|
|
480
|
+
return True, ""
|