lamindb_setup 0.76.7__py2.py3-none-any.whl → 0.77.0__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 +6 -7
- 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 +440 -433
- lamindb_setup/_delete.py +137 -137
- lamindb_setup/_django.py +41 -41
- lamindb_setup/_exportdb.py +68 -68
- lamindb_setup/_importdb.py +50 -50
- lamindb_setup/_init_instance.py +374 -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 -391
- lamindb_setup/_set_managed_storage.py +55 -55
- lamindb_setup/_setup_user.py +134 -118
- 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_client.py +173 -164
- lamindb_setup/core/_hub_core.py +524 -473
- 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 +461 -461
- lamindb_setup/core/_settings_load.py +105 -100
- lamindb_setup/core/_settings_save.py +81 -81
- lamindb_setup/core/_settings_storage.py +393 -393
- lamindb_setup/core/_settings_store.py +73 -72
- lamindb_setup/core/_settings_user.py +53 -51
- lamindb_setup/core/_setup_bionty_sources.py +101 -99
- lamindb_setup/core/cloud_sqlite_locker.py +232 -232
- lamindb_setup/core/django.py +113 -113
- 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.76.7.dist-info → lamindb_setup-0.77.0.dist-info}/METADATA +1 -1
- lamindb_setup-0.77.0.dist-info/RECORD +46 -0
- {lamindb_setup-0.76.7.dist-info → lamindb_setup-0.77.0.dist-info}/WHEEL +1 -1
- lamindb_setup-0.76.7.dist-info/RECORD +0 -46
- {lamindb_setup-0.76.7.dist-info → lamindb_setup-0.77.0.dist-info}/LICENSE +0 -0
lamindb_setup/_init_instance.py
CHANGED
|
@@ -1,374 +1,374 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import uuid
|
|
7
|
-
from typing import TYPE_CHECKING, Literal
|
|
8
|
-
from uuid import UUID
|
|
9
|
-
|
|
10
|
-
from django.core.exceptions import FieldError
|
|
11
|
-
from django.db.utils import OperationalError, ProgrammingError
|
|
12
|
-
from lamin_utils import logger
|
|
13
|
-
|
|
14
|
-
from ._close import close as close_instance
|
|
15
|
-
from ._silence_loggers import silence_loggers
|
|
16
|
-
from .core import InstanceSettings
|
|
17
|
-
from .core._settings import settings
|
|
18
|
-
from .core._settings_instance import is_local_db_url
|
|
19
|
-
from .core._settings_storage import StorageSettings, init_storage
|
|
20
|
-
from .core.upath import UPath
|
|
21
|
-
|
|
22
|
-
if TYPE_CHECKING:
|
|
23
|
-
from pydantic import PostgresDsn
|
|
24
|
-
|
|
25
|
-
from .core.types import UPathStr
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_schema_module_name(schema_name) -> str:
|
|
29
|
-
import importlib.util
|
|
30
|
-
|
|
31
|
-
name_attempts = [f"lnschema_{schema_name.replace('-', '_')}", schema_name]
|
|
32
|
-
for name in name_attempts:
|
|
33
|
-
module_spec = importlib.util.find_spec(name)
|
|
34
|
-
if module_spec is not None:
|
|
35
|
-
return name
|
|
36
|
-
raise ImportError(
|
|
37
|
-
f"Python package for '{schema_name}' is not installed.\nIf your package is on PyPI, run `pip install {schema_name}`"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def register_storage_in_instance(ssettings: StorageSettings):
|
|
42
|
-
from lnschema_core.models import Storage
|
|
43
|
-
from lnschema_core.users import current_user_id
|
|
44
|
-
|
|
45
|
-
from .core.hashing import hash_and_encode_as_b62
|
|
46
|
-
|
|
47
|
-
if ssettings._instance_id is not None:
|
|
48
|
-
instance_uid = hash_and_encode_as_b62(ssettings._instance_id.hex)[:12]
|
|
49
|
-
else:
|
|
50
|
-
instance_uid = None
|
|
51
|
-
# how do we ensure that this function is only called passing
|
|
52
|
-
# the managing instance?
|
|
53
|
-
defaults = {
|
|
54
|
-
"root": ssettings.root_as_str,
|
|
55
|
-
"type": ssettings.type,
|
|
56
|
-
"region": ssettings.region,
|
|
57
|
-
"instance_uid": instance_uid,
|
|
58
|
-
"created_by_id": current_user_id(),
|
|
59
|
-
"run": None,
|
|
60
|
-
}
|
|
61
|
-
if ssettings._uid is not None:
|
|
62
|
-
defaults["uid"] = ssettings._uid
|
|
63
|
-
storage, _ = Storage.objects.update_or_create(
|
|
64
|
-
root=ssettings.root_as_str,
|
|
65
|
-
defaults=defaults,
|
|
66
|
-
)
|
|
67
|
-
return storage
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def register_user(usettings):
|
|
71
|
-
from lnschema_core.models import User
|
|
72
|
-
|
|
73
|
-
try:
|
|
74
|
-
# need to have try except because of integer primary key migration
|
|
75
|
-
user, created = User.objects.update_or_create(
|
|
76
|
-
uid=usettings.uid,
|
|
77
|
-
defaults={
|
|
78
|
-
"handle": usettings.handle,
|
|
79
|
-
"name": usettings.name,
|
|
80
|
-
},
|
|
81
|
-
)
|
|
82
|
-
# for users with only read access, except via ProgrammingError
|
|
83
|
-
# ProgrammingError: permission denied for table lnschema_core_user
|
|
84
|
-
except (OperationalError, FieldError, ProgrammingError):
|
|
85
|
-
pass
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def register_user_and_storage_in_instance(isettings: InstanceSettings, usettings):
|
|
89
|
-
"""Register user & storage in DB."""
|
|
90
|
-
from django.db.utils import OperationalError
|
|
91
|
-
|
|
92
|
-
try:
|
|
93
|
-
register_user(usettings)
|
|
94
|
-
register_storage_in_instance(isettings.storage)
|
|
95
|
-
except OperationalError as error:
|
|
96
|
-
logger.warning(f"instance seems not set up ({error})")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def reload_schema_modules(isettings: InstanceSettings):
|
|
100
|
-
schema_names = ["core"] + list(isettings.schema)
|
|
101
|
-
schema_module_names = [get_schema_module_name(n) for n in schema_names]
|
|
102
|
-
|
|
103
|
-
for schema_module_name in schema_module_names:
|
|
104
|
-
if schema_module_name in sys.modules:
|
|
105
|
-
schema_module = importlib.import_module(schema_module_name)
|
|
106
|
-
importlib.reload(schema_module)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def reload_lamindb_itself(isettings) -> bool:
|
|
110
|
-
reloaded = False
|
|
111
|
-
if "lamindb" in sys.modules:
|
|
112
|
-
import lamindb
|
|
113
|
-
|
|
114
|
-
importlib.reload(lamindb)
|
|
115
|
-
reloaded = True
|
|
116
|
-
if "bionty" in isettings.schema and "bionty" in sys.modules:
|
|
117
|
-
schema_module = importlib.import_module("bionty")
|
|
118
|
-
importlib.reload(schema_module)
|
|
119
|
-
reloaded = True
|
|
120
|
-
return reloaded
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def reload_lamindb(isettings: InstanceSettings):
|
|
124
|
-
# only touch lamindb if we're operating from lamindb
|
|
125
|
-
reload_schema_modules(isettings)
|
|
126
|
-
log_message = settings.auto_connect
|
|
127
|
-
if not reload_lamindb_itself(isettings):
|
|
128
|
-
log_message = True
|
|
129
|
-
if log_message:
|
|
130
|
-
logger.important(f"connected lamindb: {isettings.slug}")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
ERROR_SQLITE_CACHE = """
|
|
134
|
-
Your cached local SQLite file exists, while your cloud SQLite file ({}) doesn't.
|
|
135
|
-
Either delete your cache ({}) or add it back to the cloud (if delete was accidental).
|
|
136
|
-
"""
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def process_connect_response(
|
|
140
|
-
response: tuple | str, instance_identifier: str
|
|
141
|
-
) -> tuple[
|
|
142
|
-
UUID,
|
|
143
|
-
Literal[
|
|
144
|
-
"instance-corrupted-or-deleted", "account-not-exists", "instance-not-found"
|
|
145
|
-
],
|
|
146
|
-
]:
|
|
147
|
-
# for internal use when creating instances through CICD
|
|
148
|
-
if isinstance(response, tuple) and response[0] == "instance-corrupted-or-deleted":
|
|
149
|
-
hub_result = response[1]
|
|
150
|
-
instance_state = response[0]
|
|
151
|
-
instance_id = UUID(hub_result["id"])
|
|
152
|
-
else:
|
|
153
|
-
instance_id_str = os.getenv("LAMINDB_INSTANCE_ID_INIT")
|
|
154
|
-
if instance_id_str is None:
|
|
155
|
-
instance_id = uuid.uuid5(uuid.NAMESPACE_URL, instance_identifier)
|
|
156
|
-
else:
|
|
157
|
-
instance_id = UUID(instance_id_str)
|
|
158
|
-
instance_state = response
|
|
159
|
-
return instance_id, instance_state
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def validate_init_args(
|
|
163
|
-
*,
|
|
164
|
-
storage: UPathStr,
|
|
165
|
-
name: str | None = None,
|
|
166
|
-
db: PostgresDsn | None = None,
|
|
167
|
-
schema: str | None = None,
|
|
168
|
-
_test: bool = False,
|
|
169
|
-
) -> tuple[
|
|
170
|
-
str,
|
|
171
|
-
UUID | None,
|
|
172
|
-
Literal[
|
|
173
|
-
"connected",
|
|
174
|
-
"instance-corrupted-or-deleted",
|
|
175
|
-
"account-not-exists",
|
|
176
|
-
"instance-not-found",
|
|
177
|
-
],
|
|
178
|
-
str,
|
|
179
|
-
]:
|
|
180
|
-
from ._connect_instance import connect
|
|
181
|
-
from .core._hub_utils import (
|
|
182
|
-
validate_schema_arg,
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
# should be called as the first thing
|
|
186
|
-
name_str = infer_instance_name(storage=storage, name=name, db=db)
|
|
187
|
-
# test whether instance exists by trying to load it
|
|
188
|
-
instance_slug = f"{settings.user.handle}/{name_str}"
|
|
189
|
-
response = connect(instance_slug, db=db, _raise_not_found_error=False, _test=_test)
|
|
190
|
-
instance_state: Literal[
|
|
191
|
-
"connected",
|
|
192
|
-
"instance-corrupted-or-deleted",
|
|
193
|
-
"account-not-exists",
|
|
194
|
-
"instance-not-found",
|
|
195
|
-
] = "connected"
|
|
196
|
-
instance_id = None
|
|
197
|
-
if response is not None:
|
|
198
|
-
instance_id, instance_state = process_connect_response(response, instance_slug)
|
|
199
|
-
schema = validate_schema_arg(schema)
|
|
200
|
-
return name_str, instance_id, instance_state, instance_slug
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
MESSAGE_NO_MULTIPLE_INSTANCE = """
|
|
204
|
-
Currently don't support subsequent connection to different databases in the same
|
|
205
|
-
Python session.\n
|
|
206
|
-
Try running on the CLI: lamin set auto-connect false
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def init(
|
|
211
|
-
*,
|
|
212
|
-
storage: UPathStr,
|
|
213
|
-
name: str | None = None,
|
|
214
|
-
db: PostgresDsn | None = None,
|
|
215
|
-
schema: str | None = None,
|
|
216
|
-
_test: bool = False,
|
|
217
|
-
) -> None:
|
|
218
|
-
"""Create and load a LaminDB instance.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
storage: Either ``"create-s3"``, local or
|
|
222
|
-
remote folder (``"s3://..."`` or ``"gs://..."``).
|
|
223
|
-
name: Instance name.
|
|
224
|
-
db: Database connection url, do not pass for SQLite.
|
|
225
|
-
schema: Comma-separated string of schema modules. None if not set.
|
|
226
|
-
"""
|
|
227
|
-
isettings = None
|
|
228
|
-
ssettings = None
|
|
229
|
-
try:
|
|
230
|
-
silence_loggers()
|
|
231
|
-
from ._check_setup import _check_instance_setup
|
|
232
|
-
|
|
233
|
-
if _check_instance_setup() and not _test:
|
|
234
|
-
raise RuntimeError(MESSAGE_NO_MULTIPLE_INSTANCE)
|
|
235
|
-
else:
|
|
236
|
-
close_instance(mute=True)
|
|
237
|
-
from .core._hub_core import init_instance as init_instance_hub
|
|
238
|
-
|
|
239
|
-
name_str, instance_id, instance_state, _ = validate_init_args(
|
|
240
|
-
storage=storage,
|
|
241
|
-
name=name,
|
|
242
|
-
db=db,
|
|
243
|
-
schema=schema,
|
|
244
|
-
_test=_test,
|
|
245
|
-
)
|
|
246
|
-
if instance_state == "connected":
|
|
247
|
-
settings.auto_connect = True # we can also debate this switch here
|
|
248
|
-
return None
|
|
249
|
-
# the conditions here match `isettings.is_remote`, but I currently don't
|
|
250
|
-
# see a way of making this more elegant; should become possible if we
|
|
251
|
-
# remove the instance.storage_id FK on the hub
|
|
252
|
-
prevent_register_hub = is_local_db_url(db) if db is not None else False
|
|
253
|
-
ssettings, _ = init_storage(
|
|
254
|
-
storage,
|
|
255
|
-
instance_id=instance_id,
|
|
256
|
-
init_instance=True,
|
|
257
|
-
prevent_register_hub=prevent_register_hub,
|
|
258
|
-
)
|
|
259
|
-
isettings = InstanceSettings(
|
|
260
|
-
id=instance_id, # type: ignore
|
|
261
|
-
owner=settings.user.handle,
|
|
262
|
-
name=name_str,
|
|
263
|
-
storage=ssettings,
|
|
264
|
-
db=db,
|
|
265
|
-
schema=schema,
|
|
266
|
-
uid=ssettings.uid,
|
|
267
|
-
)
|
|
268
|
-
register_on_hub = (
|
|
269
|
-
isettings.is_remote and instance_state != "instance-corrupted-or-deleted"
|
|
270
|
-
)
|
|
271
|
-
if register_on_hub:
|
|
272
|
-
init_instance_hub(isettings)
|
|
273
|
-
validate_sqlite_state(isettings)
|
|
274
|
-
isettings._persist()
|
|
275
|
-
if _test:
|
|
276
|
-
return None
|
|
277
|
-
isettings._init_db()
|
|
278
|
-
load_from_isettings(isettings, init=True)
|
|
279
|
-
if isettings._is_cloud_sqlite:
|
|
280
|
-
isettings._cloud_sqlite_locker.lock()
|
|
281
|
-
logger.warning(
|
|
282
|
-
"locked instance (to unlock and push changes to the cloud SQLite file,"
|
|
283
|
-
" call: lamin close)"
|
|
284
|
-
)
|
|
285
|
-
if register_on_hub and isettings.dialect != "sqlite":
|
|
286
|
-
from ._schema_metadata import update_schema_in_hub
|
|
287
|
-
|
|
288
|
-
update_schema_in_hub()
|
|
289
|
-
settings.auto_connect = True
|
|
290
|
-
except Exception as e:
|
|
291
|
-
from ._delete import delete_by_isettings
|
|
292
|
-
from .core._hub_core import delete_instance_record, delete_storage_record
|
|
293
|
-
|
|
294
|
-
if isettings is not None:
|
|
295
|
-
delete_by_isettings(isettings)
|
|
296
|
-
if settings.user.handle != "anonymous" and isettings.is_on_hub:
|
|
297
|
-
delete_instance_record(isettings._id)
|
|
298
|
-
isettings._get_settings_file().unlink(missing_ok=True) # type: ignore
|
|
299
|
-
if (
|
|
300
|
-
ssettings is not None
|
|
301
|
-
and settings.user.handle != "anonymous"
|
|
302
|
-
and ssettings.is_on_hub
|
|
303
|
-
):
|
|
304
|
-
delete_storage_record(ssettings._uuid) # type: ignore
|
|
305
|
-
raise e
|
|
306
|
-
return None
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def load_from_isettings(
|
|
310
|
-
isettings: InstanceSettings,
|
|
311
|
-
*,
|
|
312
|
-
init: bool = False,
|
|
313
|
-
) -> None:
|
|
314
|
-
from .core._setup_bionty_sources import load_bionty_sources, write_bionty_sources
|
|
315
|
-
|
|
316
|
-
if init:
|
|
317
|
-
# during init both user and storage need to be registered
|
|
318
|
-
register_user_and_storage_in_instance(isettings, settings.user)
|
|
319
|
-
write_bionty_sources(isettings)
|
|
320
|
-
isettings._update_cloud_sqlite_file(unlock_cloud_sqlite=False)
|
|
321
|
-
else:
|
|
322
|
-
# when loading, django is already set up
|
|
323
|
-
#
|
|
324
|
-
# only register user if the instance is connected
|
|
325
|
-
# for the first time in an environment
|
|
326
|
-
# this is our best proxy for that the user might not
|
|
327
|
-
# yet be registered
|
|
328
|
-
if not isettings._get_settings_file().exists():
|
|
329
|
-
register_user(settings.user)
|
|
330
|
-
load_bionty_sources(isettings)
|
|
331
|
-
isettings._persist()
|
|
332
|
-
reload_lamindb(isettings)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
def validate_sqlite_state(isettings: InstanceSettings) -> None:
|
|
336
|
-
if isettings._is_cloud_sqlite:
|
|
337
|
-
if (
|
|
338
|
-
# it's important to first evaluate the existence check
|
|
339
|
-
# for the local sqlite file because it doesn't need a network
|
|
340
|
-
# request
|
|
341
|
-
isettings._sqlite_file_local.exists()
|
|
342
|
-
and not isettings._sqlite_file.exists()
|
|
343
|
-
):
|
|
344
|
-
raise RuntimeError(
|
|
345
|
-
ERROR_SQLITE_CACHE.format(
|
|
346
|
-
isettings._sqlite_file, isettings._sqlite_file_local
|
|
347
|
-
)
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
def infer_instance_name(
|
|
352
|
-
*,
|
|
353
|
-
storage: UPathStr,
|
|
354
|
-
name: str | None = None,
|
|
355
|
-
db: PostgresDsn | None = None,
|
|
356
|
-
) -> str:
|
|
357
|
-
if name is not None:
|
|
358
|
-
if "/" in name:
|
|
359
|
-
raise ValueError("Invalid instance name: '/' delimiter not allowed.")
|
|
360
|
-
return name
|
|
361
|
-
if db is not None:
|
|
362
|
-
logger.warning("using the sql database name for the instance name")
|
|
363
|
-
# this isn't a great way to access the db name
|
|
364
|
-
# could use LaminDsn instead
|
|
365
|
-
return str(db).split("/")[-1]
|
|
366
|
-
if storage == "create-s3":
|
|
367
|
-
raise ValueError("pass name to init if storage = 'create-s3'")
|
|
368
|
-
storage_path = UPath(storage)
|
|
369
|
-
if storage_path.name != "":
|
|
370
|
-
name = storage_path.name
|
|
371
|
-
else:
|
|
372
|
-
# dedicated treatment of bucket names
|
|
373
|
-
name = storage_path._url.netloc
|
|
374
|
-
return name.lower()
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import uuid
|
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from django.core.exceptions import FieldError
|
|
11
|
+
from django.db.utils import OperationalError, ProgrammingError
|
|
12
|
+
from lamin_utils import logger
|
|
13
|
+
|
|
14
|
+
from ._close import close as close_instance
|
|
15
|
+
from ._silence_loggers import silence_loggers
|
|
16
|
+
from .core import InstanceSettings
|
|
17
|
+
from .core._settings import settings
|
|
18
|
+
from .core._settings_instance import is_local_db_url
|
|
19
|
+
from .core._settings_storage import StorageSettings, init_storage
|
|
20
|
+
from .core.upath import UPath
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from pydantic import PostgresDsn
|
|
24
|
+
|
|
25
|
+
from .core.types import UPathStr
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_schema_module_name(schema_name) -> str:
|
|
29
|
+
import importlib.util
|
|
30
|
+
|
|
31
|
+
name_attempts = [f"lnschema_{schema_name.replace('-', '_')}", schema_name]
|
|
32
|
+
for name in name_attempts:
|
|
33
|
+
module_spec = importlib.util.find_spec(name)
|
|
34
|
+
if module_spec is not None:
|
|
35
|
+
return name
|
|
36
|
+
raise ImportError(
|
|
37
|
+
f"Python package for '{schema_name}' is not installed.\nIf your package is on PyPI, run `pip install {schema_name}`"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register_storage_in_instance(ssettings: StorageSettings):
|
|
42
|
+
from lnschema_core.models import Storage
|
|
43
|
+
from lnschema_core.users import current_user_id
|
|
44
|
+
|
|
45
|
+
from .core.hashing import hash_and_encode_as_b62
|
|
46
|
+
|
|
47
|
+
if ssettings._instance_id is not None:
|
|
48
|
+
instance_uid = hash_and_encode_as_b62(ssettings._instance_id.hex)[:12]
|
|
49
|
+
else:
|
|
50
|
+
instance_uid = None
|
|
51
|
+
# how do we ensure that this function is only called passing
|
|
52
|
+
# the managing instance?
|
|
53
|
+
defaults = {
|
|
54
|
+
"root": ssettings.root_as_str,
|
|
55
|
+
"type": ssettings.type,
|
|
56
|
+
"region": ssettings.region,
|
|
57
|
+
"instance_uid": instance_uid,
|
|
58
|
+
"created_by_id": current_user_id(),
|
|
59
|
+
"run": None,
|
|
60
|
+
}
|
|
61
|
+
if ssettings._uid is not None:
|
|
62
|
+
defaults["uid"] = ssettings._uid
|
|
63
|
+
storage, _ = Storage.objects.update_or_create(
|
|
64
|
+
root=ssettings.root_as_str,
|
|
65
|
+
defaults=defaults,
|
|
66
|
+
)
|
|
67
|
+
return storage
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def register_user(usettings):
|
|
71
|
+
from lnschema_core.models import User
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# need to have try except because of integer primary key migration
|
|
75
|
+
user, created = User.objects.update_or_create(
|
|
76
|
+
uid=usettings.uid,
|
|
77
|
+
defaults={
|
|
78
|
+
"handle": usettings.handle,
|
|
79
|
+
"name": usettings.name,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
# for users with only read access, except via ProgrammingError
|
|
83
|
+
# ProgrammingError: permission denied for table lnschema_core_user
|
|
84
|
+
except (OperationalError, FieldError, ProgrammingError):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def register_user_and_storage_in_instance(isettings: InstanceSettings, usettings):
|
|
89
|
+
"""Register user & storage in DB."""
|
|
90
|
+
from django.db.utils import OperationalError
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
register_user(usettings)
|
|
94
|
+
register_storage_in_instance(isettings.storage)
|
|
95
|
+
except OperationalError as error:
|
|
96
|
+
logger.warning(f"instance seems not set up ({error})")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def reload_schema_modules(isettings: InstanceSettings):
|
|
100
|
+
schema_names = ["core"] + list(isettings.schema)
|
|
101
|
+
schema_module_names = [get_schema_module_name(n) for n in schema_names]
|
|
102
|
+
|
|
103
|
+
for schema_module_name in schema_module_names:
|
|
104
|
+
if schema_module_name in sys.modules:
|
|
105
|
+
schema_module = importlib.import_module(schema_module_name)
|
|
106
|
+
importlib.reload(schema_module)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def reload_lamindb_itself(isettings) -> bool:
|
|
110
|
+
reloaded = False
|
|
111
|
+
if "lamindb" in sys.modules:
|
|
112
|
+
import lamindb
|
|
113
|
+
|
|
114
|
+
importlib.reload(lamindb)
|
|
115
|
+
reloaded = True
|
|
116
|
+
if "bionty" in isettings.schema and "bionty" in sys.modules:
|
|
117
|
+
schema_module = importlib.import_module("bionty")
|
|
118
|
+
importlib.reload(schema_module)
|
|
119
|
+
reloaded = True
|
|
120
|
+
return reloaded
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def reload_lamindb(isettings: InstanceSettings):
|
|
124
|
+
# only touch lamindb if we're operating from lamindb
|
|
125
|
+
reload_schema_modules(isettings)
|
|
126
|
+
log_message = settings.auto_connect
|
|
127
|
+
if not reload_lamindb_itself(isettings):
|
|
128
|
+
log_message = True
|
|
129
|
+
if log_message:
|
|
130
|
+
logger.important(f"connected lamindb: {isettings.slug}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
ERROR_SQLITE_CACHE = """
|
|
134
|
+
Your cached local SQLite file exists, while your cloud SQLite file ({}) doesn't.
|
|
135
|
+
Either delete your cache ({}) or add it back to the cloud (if delete was accidental).
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def process_connect_response(
|
|
140
|
+
response: tuple | str, instance_identifier: str
|
|
141
|
+
) -> tuple[
|
|
142
|
+
UUID,
|
|
143
|
+
Literal[
|
|
144
|
+
"instance-corrupted-or-deleted", "account-not-exists", "instance-not-found"
|
|
145
|
+
],
|
|
146
|
+
]:
|
|
147
|
+
# for internal use when creating instances through CICD
|
|
148
|
+
if isinstance(response, tuple) and response[0] == "instance-corrupted-or-deleted":
|
|
149
|
+
hub_result = response[1]
|
|
150
|
+
instance_state = response[0]
|
|
151
|
+
instance_id = UUID(hub_result["id"])
|
|
152
|
+
else:
|
|
153
|
+
instance_id_str = os.getenv("LAMINDB_INSTANCE_ID_INIT")
|
|
154
|
+
if instance_id_str is None:
|
|
155
|
+
instance_id = uuid.uuid5(uuid.NAMESPACE_URL, instance_identifier)
|
|
156
|
+
else:
|
|
157
|
+
instance_id = UUID(instance_id_str)
|
|
158
|
+
instance_state = response
|
|
159
|
+
return instance_id, instance_state
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def validate_init_args(
|
|
163
|
+
*,
|
|
164
|
+
storage: UPathStr,
|
|
165
|
+
name: str | None = None,
|
|
166
|
+
db: PostgresDsn | None = None,
|
|
167
|
+
schema: str | None = None,
|
|
168
|
+
_test: bool = False,
|
|
169
|
+
) -> tuple[
|
|
170
|
+
str,
|
|
171
|
+
UUID | None,
|
|
172
|
+
Literal[
|
|
173
|
+
"connected",
|
|
174
|
+
"instance-corrupted-or-deleted",
|
|
175
|
+
"account-not-exists",
|
|
176
|
+
"instance-not-found",
|
|
177
|
+
],
|
|
178
|
+
str,
|
|
179
|
+
]:
|
|
180
|
+
from ._connect_instance import connect
|
|
181
|
+
from .core._hub_utils import (
|
|
182
|
+
validate_schema_arg,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# should be called as the first thing
|
|
186
|
+
name_str = infer_instance_name(storage=storage, name=name, db=db)
|
|
187
|
+
# test whether instance exists by trying to load it
|
|
188
|
+
instance_slug = f"{settings.user.handle}/{name_str}"
|
|
189
|
+
response = connect(instance_slug, db=db, _raise_not_found_error=False, _test=_test)
|
|
190
|
+
instance_state: Literal[
|
|
191
|
+
"connected",
|
|
192
|
+
"instance-corrupted-or-deleted",
|
|
193
|
+
"account-not-exists",
|
|
194
|
+
"instance-not-found",
|
|
195
|
+
] = "connected"
|
|
196
|
+
instance_id = None
|
|
197
|
+
if response is not None:
|
|
198
|
+
instance_id, instance_state = process_connect_response(response, instance_slug)
|
|
199
|
+
schema = validate_schema_arg(schema)
|
|
200
|
+
return name_str, instance_id, instance_state, instance_slug
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
MESSAGE_NO_MULTIPLE_INSTANCE = """
|
|
204
|
+
Currently don't support subsequent connection to different databases in the same
|
|
205
|
+
Python session.\n
|
|
206
|
+
Try running on the CLI: lamin set auto-connect false
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def init(
|
|
211
|
+
*,
|
|
212
|
+
storage: UPathStr,
|
|
213
|
+
name: str | None = None,
|
|
214
|
+
db: PostgresDsn | None = None,
|
|
215
|
+
schema: str | None = None,
|
|
216
|
+
_test: bool = False,
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Create and load a LaminDB instance.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
storage: Either ``"create-s3"``, local or
|
|
222
|
+
remote folder (``"s3://..."`` or ``"gs://..."``).
|
|
223
|
+
name: Instance name.
|
|
224
|
+
db: Database connection url, do not pass for SQLite.
|
|
225
|
+
schema: Comma-separated string of schema modules. None if not set.
|
|
226
|
+
"""
|
|
227
|
+
isettings = None
|
|
228
|
+
ssettings = None
|
|
229
|
+
try:
|
|
230
|
+
silence_loggers()
|
|
231
|
+
from ._check_setup import _check_instance_setup
|
|
232
|
+
|
|
233
|
+
if _check_instance_setup() and not _test:
|
|
234
|
+
raise RuntimeError(MESSAGE_NO_MULTIPLE_INSTANCE)
|
|
235
|
+
else:
|
|
236
|
+
close_instance(mute=True)
|
|
237
|
+
from .core._hub_core import init_instance as init_instance_hub
|
|
238
|
+
|
|
239
|
+
name_str, instance_id, instance_state, _ = validate_init_args(
|
|
240
|
+
storage=storage,
|
|
241
|
+
name=name,
|
|
242
|
+
db=db,
|
|
243
|
+
schema=schema,
|
|
244
|
+
_test=_test,
|
|
245
|
+
)
|
|
246
|
+
if instance_state == "connected":
|
|
247
|
+
settings.auto_connect = True # we can also debate this switch here
|
|
248
|
+
return None
|
|
249
|
+
# the conditions here match `isettings.is_remote`, but I currently don't
|
|
250
|
+
# see a way of making this more elegant; should become possible if we
|
|
251
|
+
# remove the instance.storage_id FK on the hub
|
|
252
|
+
prevent_register_hub = is_local_db_url(db) if db is not None else False
|
|
253
|
+
ssettings, _ = init_storage(
|
|
254
|
+
storage,
|
|
255
|
+
instance_id=instance_id,
|
|
256
|
+
init_instance=True,
|
|
257
|
+
prevent_register_hub=prevent_register_hub,
|
|
258
|
+
)
|
|
259
|
+
isettings = InstanceSettings(
|
|
260
|
+
id=instance_id, # type: ignore
|
|
261
|
+
owner=settings.user.handle,
|
|
262
|
+
name=name_str,
|
|
263
|
+
storage=ssettings,
|
|
264
|
+
db=db,
|
|
265
|
+
schema=schema,
|
|
266
|
+
uid=ssettings.uid,
|
|
267
|
+
)
|
|
268
|
+
register_on_hub = (
|
|
269
|
+
isettings.is_remote and instance_state != "instance-corrupted-or-deleted"
|
|
270
|
+
)
|
|
271
|
+
if register_on_hub:
|
|
272
|
+
init_instance_hub(isettings)
|
|
273
|
+
validate_sqlite_state(isettings)
|
|
274
|
+
isettings._persist()
|
|
275
|
+
if _test:
|
|
276
|
+
return None
|
|
277
|
+
isettings._init_db()
|
|
278
|
+
load_from_isettings(isettings, init=True)
|
|
279
|
+
if isettings._is_cloud_sqlite:
|
|
280
|
+
isettings._cloud_sqlite_locker.lock()
|
|
281
|
+
logger.warning(
|
|
282
|
+
"locked instance (to unlock and push changes to the cloud SQLite file,"
|
|
283
|
+
" call: lamin close)"
|
|
284
|
+
)
|
|
285
|
+
if register_on_hub and isettings.dialect != "sqlite":
|
|
286
|
+
from ._schema_metadata import update_schema_in_hub
|
|
287
|
+
|
|
288
|
+
update_schema_in_hub()
|
|
289
|
+
settings.auto_connect = True
|
|
290
|
+
except Exception as e:
|
|
291
|
+
from ._delete import delete_by_isettings
|
|
292
|
+
from .core._hub_core import delete_instance_record, delete_storage_record
|
|
293
|
+
|
|
294
|
+
if isettings is not None:
|
|
295
|
+
delete_by_isettings(isettings)
|
|
296
|
+
if settings.user.handle != "anonymous" and isettings.is_on_hub:
|
|
297
|
+
delete_instance_record(isettings._id)
|
|
298
|
+
isettings._get_settings_file().unlink(missing_ok=True) # type: ignore
|
|
299
|
+
if (
|
|
300
|
+
ssettings is not None
|
|
301
|
+
and settings.user.handle != "anonymous"
|
|
302
|
+
and ssettings.is_on_hub
|
|
303
|
+
):
|
|
304
|
+
delete_storage_record(ssettings._uuid) # type: ignore
|
|
305
|
+
raise e
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def load_from_isettings(
|
|
310
|
+
isettings: InstanceSettings,
|
|
311
|
+
*,
|
|
312
|
+
init: bool = False,
|
|
313
|
+
) -> None:
|
|
314
|
+
from .core._setup_bionty_sources import load_bionty_sources, write_bionty_sources
|
|
315
|
+
|
|
316
|
+
if init:
|
|
317
|
+
# during init both user and storage need to be registered
|
|
318
|
+
register_user_and_storage_in_instance(isettings, settings.user)
|
|
319
|
+
write_bionty_sources(isettings)
|
|
320
|
+
isettings._update_cloud_sqlite_file(unlock_cloud_sqlite=False)
|
|
321
|
+
else:
|
|
322
|
+
# when loading, django is already set up
|
|
323
|
+
#
|
|
324
|
+
# only register user if the instance is connected
|
|
325
|
+
# for the first time in an environment
|
|
326
|
+
# this is our best proxy for that the user might not
|
|
327
|
+
# yet be registered
|
|
328
|
+
if not isettings._get_settings_file().exists():
|
|
329
|
+
register_user(settings.user)
|
|
330
|
+
load_bionty_sources(isettings)
|
|
331
|
+
isettings._persist()
|
|
332
|
+
reload_lamindb(isettings)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def validate_sqlite_state(isettings: InstanceSettings) -> None:
|
|
336
|
+
if isettings._is_cloud_sqlite:
|
|
337
|
+
if (
|
|
338
|
+
# it's important to first evaluate the existence check
|
|
339
|
+
# for the local sqlite file because it doesn't need a network
|
|
340
|
+
# request
|
|
341
|
+
isettings._sqlite_file_local.exists()
|
|
342
|
+
and not isettings._sqlite_file.exists()
|
|
343
|
+
):
|
|
344
|
+
raise RuntimeError(
|
|
345
|
+
ERROR_SQLITE_CACHE.format(
|
|
346
|
+
isettings._sqlite_file, isettings._sqlite_file_local
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def infer_instance_name(
|
|
352
|
+
*,
|
|
353
|
+
storage: UPathStr,
|
|
354
|
+
name: str | None = None,
|
|
355
|
+
db: PostgresDsn | None = None,
|
|
356
|
+
) -> str:
|
|
357
|
+
if name is not None:
|
|
358
|
+
if "/" in name:
|
|
359
|
+
raise ValueError("Invalid instance name: '/' delimiter not allowed.")
|
|
360
|
+
return name
|
|
361
|
+
if db is not None:
|
|
362
|
+
logger.warning("using the sql database name for the instance name")
|
|
363
|
+
# this isn't a great way to access the db name
|
|
364
|
+
# could use LaminDsn instead
|
|
365
|
+
return str(db).split("/")[-1]
|
|
366
|
+
if storage == "create-s3":
|
|
367
|
+
raise ValueError("pass name to init if storage = 'create-s3'")
|
|
368
|
+
storage_path = UPath(storage)
|
|
369
|
+
if storage_path.name != "":
|
|
370
|
+
name = storage_path.name
|
|
371
|
+
else:
|
|
372
|
+
# dedicated treatment of bucket names
|
|
373
|
+
name = storage_path._url.netloc
|
|
374
|
+
return name.lower()
|