lamindb_setup 0.78.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lamindb_setup/__init__.py +74 -0
- lamindb_setup/_cache.py +48 -0
- lamindb_setup/_check.py +7 -0
- lamindb_setup/_check_setup.py +92 -0
- lamindb_setup/_close.py +35 -0
- lamindb_setup/_connect_instance.py +429 -0
- lamindb_setup/_delete.py +141 -0
- lamindb_setup/_django.py +39 -0
- lamindb_setup/_entry_points.py +22 -0
- lamindb_setup/_exportdb.py +68 -0
- lamindb_setup/_importdb.py +50 -0
- lamindb_setup/_init_instance.py +411 -0
- lamindb_setup/_migrate.py +239 -0
- lamindb_setup/_register_instance.py +36 -0
- lamindb_setup/_schema.py +27 -0
- lamindb_setup/_schema_metadata.py +411 -0
- lamindb_setup/_set_managed_storage.py +55 -0
- lamindb_setup/_setup_user.py +137 -0
- lamindb_setup/_silence_loggers.py +44 -0
- lamindb_setup/core/__init__.py +21 -0
- lamindb_setup/core/_aws_credentials.py +151 -0
- lamindb_setup/core/_aws_storage.py +48 -0
- lamindb_setup/core/_deprecated.py +55 -0
- lamindb_setup/core/_docs.py +14 -0
- lamindb_setup/core/_hub_client.py +173 -0
- lamindb_setup/core/_hub_core.py +554 -0
- lamindb_setup/core/_hub_crud.py +211 -0
- lamindb_setup/core/_hub_utils.py +109 -0
- lamindb_setup/core/_private_django_api.py +88 -0
- lamindb_setup/core/_settings.py +184 -0
- lamindb_setup/core/_settings_instance.py +485 -0
- lamindb_setup/core/_settings_load.py +117 -0
- lamindb_setup/core/_settings_save.py +92 -0
- lamindb_setup/core/_settings_storage.py +350 -0
- lamindb_setup/core/_settings_store.py +75 -0
- lamindb_setup/core/_settings_user.py +55 -0
- lamindb_setup/core/_setup_bionty_sources.py +101 -0
- lamindb_setup/core/cloud_sqlite_locker.py +237 -0
- lamindb_setup/core/django.py +115 -0
- lamindb_setup/core/exceptions.py +10 -0
- lamindb_setup/core/hashing.py +116 -0
- lamindb_setup/core/types.py +17 -0
- lamindb_setup/core/upath.py +779 -0
- lamindb_setup-0.78.0.dist-info/LICENSE +201 -0
- lamindb_setup-0.78.0.dist-info/METADATA +47 -0
- lamindb_setup-0.78.0.dist-info/RECORD +47 -0
- lamindb_setup-0.78.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,411 @@
|
|
|
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._settings_user import UserSettings
|
|
26
|
+
from .core.types import UPathStr
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_schema_module_name(schema_name) -> str:
|
|
30
|
+
import importlib.util
|
|
31
|
+
|
|
32
|
+
name_attempts = [f"lnschema_{schema_name.replace('-', '_')}", schema_name]
|
|
33
|
+
for name in name_attempts:
|
|
34
|
+
module_spec = importlib.util.find_spec(name)
|
|
35
|
+
if module_spec is not None:
|
|
36
|
+
return name
|
|
37
|
+
raise ImportError(
|
|
38
|
+
f"Python package for '{schema_name}' is not installed.\nIf your package is on PyPI, run `pip install {schema_name}`"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def register_storage_in_instance(ssettings: StorageSettings):
|
|
43
|
+
from lnschema_core.models import Storage
|
|
44
|
+
from lnschema_core.users import current_user_id
|
|
45
|
+
|
|
46
|
+
from .core.hashing import hash_and_encode_as_b62
|
|
47
|
+
|
|
48
|
+
if ssettings._instance_id is not None:
|
|
49
|
+
instance_uid = hash_and_encode_as_b62(ssettings._instance_id.hex)[:12]
|
|
50
|
+
else:
|
|
51
|
+
instance_uid = None
|
|
52
|
+
# how do we ensure that this function is only called passing
|
|
53
|
+
# the managing instance?
|
|
54
|
+
defaults = {
|
|
55
|
+
"root": ssettings.root_as_str,
|
|
56
|
+
"type": ssettings.type,
|
|
57
|
+
"region": ssettings.region,
|
|
58
|
+
"instance_uid": instance_uid,
|
|
59
|
+
"created_by_id": current_user_id(),
|
|
60
|
+
"run": None,
|
|
61
|
+
}
|
|
62
|
+
if ssettings._uid is not None:
|
|
63
|
+
defaults["uid"] = ssettings._uid
|
|
64
|
+
storage, _ = Storage.objects.update_or_create(
|
|
65
|
+
root=ssettings.root_as_str,
|
|
66
|
+
defaults=defaults,
|
|
67
|
+
)
|
|
68
|
+
return storage
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def register_user(usettings):
|
|
72
|
+
from lnschema_core.models import User
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# need to have try except because of integer primary key migration
|
|
76
|
+
user, created = User.objects.update_or_create(
|
|
77
|
+
uid=usettings.uid,
|
|
78
|
+
defaults={
|
|
79
|
+
"handle": usettings.handle,
|
|
80
|
+
"name": usettings.name,
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
# for users with only read access, except via ProgrammingError
|
|
84
|
+
# ProgrammingError: permission denied for table lnschema_core_user
|
|
85
|
+
except (OperationalError, FieldError, ProgrammingError):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def register_user_and_storage_in_instance(isettings: InstanceSettings, usettings):
|
|
90
|
+
"""Register user & storage in DB."""
|
|
91
|
+
from django.db.utils import OperationalError
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
register_user(usettings)
|
|
95
|
+
register_storage_in_instance(isettings.storage)
|
|
96
|
+
except OperationalError as error:
|
|
97
|
+
logger.warning(f"instance seems not set up ({error})")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def reload_schema_modules(isettings: InstanceSettings, include_core: bool = True):
|
|
101
|
+
schema_names = ["core"] if include_core else []
|
|
102
|
+
# schema_names += list(isettings.schema)
|
|
103
|
+
schema_module_names = [get_schema_module_name(n) for n in schema_names]
|
|
104
|
+
|
|
105
|
+
for schema_module_name in schema_module_names:
|
|
106
|
+
if schema_module_name in sys.modules:
|
|
107
|
+
schema_module = importlib.import_module(schema_module_name)
|
|
108
|
+
importlib.reload(schema_module)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def reload_lamindb_itself(isettings) -> bool:
|
|
112
|
+
reloaded = False
|
|
113
|
+
if "lamindb" in sys.modules:
|
|
114
|
+
import lamindb
|
|
115
|
+
|
|
116
|
+
importlib.reload(lamindb)
|
|
117
|
+
reloaded = True
|
|
118
|
+
return reloaded
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def reload_lamindb(isettings: InstanceSettings):
|
|
122
|
+
log_message = settings.auto_connect
|
|
123
|
+
if not reload_lamindb_itself(isettings):
|
|
124
|
+
log_message = True
|
|
125
|
+
if log_message:
|
|
126
|
+
logger.important(f"connected lamindb: {isettings.slug}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
ERROR_SQLITE_CACHE = """
|
|
130
|
+
Your cached local SQLite file exists, while your cloud SQLite file ({}) doesn't.
|
|
131
|
+
Either delete your cache ({}) or add it back to the cloud (if delete was accidental).
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def process_connect_response(
|
|
136
|
+
response: tuple | str, instance_identifier: str
|
|
137
|
+
) -> tuple[
|
|
138
|
+
UUID,
|
|
139
|
+
Literal[
|
|
140
|
+
"instance-corrupted-or-deleted", "account-not-exists", "instance-not-found"
|
|
141
|
+
],
|
|
142
|
+
]:
|
|
143
|
+
# for internal use when creating instances through CICD
|
|
144
|
+
if isinstance(response, tuple) and response[0] == "instance-corrupted-or-deleted":
|
|
145
|
+
hub_result = response[1]
|
|
146
|
+
instance_state = response[0]
|
|
147
|
+
instance_id = UUID(hub_result["id"])
|
|
148
|
+
else:
|
|
149
|
+
instance_id_str = os.getenv("LAMINDB_INSTANCE_ID_INIT")
|
|
150
|
+
if instance_id_str is None:
|
|
151
|
+
instance_id = uuid.uuid5(uuid.NAMESPACE_URL, instance_identifier)
|
|
152
|
+
else:
|
|
153
|
+
instance_id = UUID(instance_id_str)
|
|
154
|
+
instance_state = response
|
|
155
|
+
return instance_id, instance_state
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def validate_init_args(
|
|
159
|
+
*,
|
|
160
|
+
storage: UPathStr,
|
|
161
|
+
name: str | None = None,
|
|
162
|
+
db: PostgresDsn | None = None,
|
|
163
|
+
schema: str | None = None,
|
|
164
|
+
_test: bool = False,
|
|
165
|
+
_write_settings: bool = True,
|
|
166
|
+
_user: UserSettings | None = None,
|
|
167
|
+
) -> tuple[
|
|
168
|
+
str,
|
|
169
|
+
UUID | None,
|
|
170
|
+
Literal[
|
|
171
|
+
"connected",
|
|
172
|
+
"instance-corrupted-or-deleted",
|
|
173
|
+
"account-not-exists",
|
|
174
|
+
"instance-not-found",
|
|
175
|
+
],
|
|
176
|
+
str,
|
|
177
|
+
]:
|
|
178
|
+
from ._connect_instance import connect
|
|
179
|
+
from .core._hub_utils import (
|
|
180
|
+
validate_schema_arg,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# should be called as the first thing
|
|
184
|
+
name_str = infer_instance_name(storage=storage, name=name, db=db)
|
|
185
|
+
owner_str = settings.user.handle if _user is None else _user.handle
|
|
186
|
+
# test whether instance exists by trying to load it
|
|
187
|
+
instance_slug = f"{owner_str}/{name_str}"
|
|
188
|
+
response = connect(
|
|
189
|
+
instance_slug,
|
|
190
|
+
_db=db,
|
|
191
|
+
_raise_not_found_error=False,
|
|
192
|
+
_test=_test,
|
|
193
|
+
_write_settings=_write_settings,
|
|
194
|
+
_user=_user,
|
|
195
|
+
)
|
|
196
|
+
instance_state: Literal[
|
|
197
|
+
"connected",
|
|
198
|
+
"instance-corrupted-or-deleted",
|
|
199
|
+
"account-not-exists",
|
|
200
|
+
"instance-not-found",
|
|
201
|
+
] = "connected"
|
|
202
|
+
instance_id = None
|
|
203
|
+
if response is not None:
|
|
204
|
+
instance_id, instance_state = process_connect_response(response, instance_slug)
|
|
205
|
+
schema = validate_schema_arg(schema)
|
|
206
|
+
return name_str, instance_id, instance_state, instance_slug
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
MESSAGE_NO_MULTIPLE_INSTANCE = """
|
|
210
|
+
Currently don't support subsequent connection to different databases in the same
|
|
211
|
+
Python session.\n
|
|
212
|
+
Try running on the CLI: lamin settings set auto-connect false
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def init(
|
|
217
|
+
*,
|
|
218
|
+
storage: UPathStr,
|
|
219
|
+
name: str | None = None,
|
|
220
|
+
db: PostgresDsn | None = None,
|
|
221
|
+
schema: str | None = None,
|
|
222
|
+
**kwargs,
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Create and load a LaminDB instance.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
storage: Either ``"create-s3"``, local or
|
|
228
|
+
remote folder (``"s3://..."`` or ``"gs://..."``).
|
|
229
|
+
name: Instance name.
|
|
230
|
+
db: Database connection url, do not pass for SQLite.
|
|
231
|
+
schema: Comma-separated string of schema modules. None if not set.
|
|
232
|
+
"""
|
|
233
|
+
isettings = None
|
|
234
|
+
ssettings = None
|
|
235
|
+
|
|
236
|
+
_write_settings: bool = kwargs.get("_write_settings", True)
|
|
237
|
+
_test: bool = kwargs.get("_test", False)
|
|
238
|
+
|
|
239
|
+
# use this user instead of settings.user
|
|
240
|
+
# contains access_token
|
|
241
|
+
_user: UserSettings | None = kwargs.get("_user", None)
|
|
242
|
+
user_handle: str = settings.user.handle if _user is None else _user.handle
|
|
243
|
+
user__uuid: UUID = settings.user._uuid if _user is None else _user._uuid # type: ignore
|
|
244
|
+
access_token: str | None = None if _user is None else _user.access_token
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
silence_loggers()
|
|
248
|
+
from ._check_setup import _check_instance_setup
|
|
249
|
+
|
|
250
|
+
if _check_instance_setup() and not _test:
|
|
251
|
+
raise RuntimeError(MESSAGE_NO_MULTIPLE_INSTANCE)
|
|
252
|
+
elif _write_settings:
|
|
253
|
+
close_instance(mute=True)
|
|
254
|
+
from .core._hub_core import init_instance as init_instance_hub
|
|
255
|
+
|
|
256
|
+
name_str, instance_id, instance_state, _ = validate_init_args(
|
|
257
|
+
storage=storage,
|
|
258
|
+
name=name,
|
|
259
|
+
db=db,
|
|
260
|
+
schema=schema,
|
|
261
|
+
_test=_test,
|
|
262
|
+
_write_settings=_write_settings,
|
|
263
|
+
_user=_user, # will get from settings.user if _user is None
|
|
264
|
+
)
|
|
265
|
+
if instance_state == "connected":
|
|
266
|
+
if _write_settings:
|
|
267
|
+
settings.auto_connect = True # we can also debate this switch here
|
|
268
|
+
return None
|
|
269
|
+
# the conditions here match `isettings.is_remote`, but I currently don't
|
|
270
|
+
# see a way of making this more elegant; should become possible if we
|
|
271
|
+
# remove the instance.storage_id FK on the hub
|
|
272
|
+
prevent_register_hub = is_local_db_url(db) if db is not None else False
|
|
273
|
+
ssettings, _ = init_storage(
|
|
274
|
+
storage,
|
|
275
|
+
instance_id=instance_id,
|
|
276
|
+
init_instance=True,
|
|
277
|
+
prevent_register_hub=prevent_register_hub,
|
|
278
|
+
created_by=user__uuid,
|
|
279
|
+
access_token=access_token,
|
|
280
|
+
)
|
|
281
|
+
isettings = InstanceSettings(
|
|
282
|
+
id=instance_id, # type: ignore
|
|
283
|
+
owner=user_handle,
|
|
284
|
+
name=name_str,
|
|
285
|
+
storage=ssettings,
|
|
286
|
+
db=db,
|
|
287
|
+
schema=schema,
|
|
288
|
+
uid=ssettings.uid,
|
|
289
|
+
# to lock passed user in isettings._cloud_sqlite_locker.lock()
|
|
290
|
+
_locker_user=_user, # only has effect if cloud sqlite
|
|
291
|
+
)
|
|
292
|
+
register_on_hub = (
|
|
293
|
+
isettings.is_remote and instance_state != "instance-corrupted-or-deleted"
|
|
294
|
+
)
|
|
295
|
+
if register_on_hub:
|
|
296
|
+
init_instance_hub(
|
|
297
|
+
isettings, account_id=user__uuid, access_token=access_token
|
|
298
|
+
)
|
|
299
|
+
validate_sqlite_state(isettings)
|
|
300
|
+
# why call it here if it is also called in load_from_isettings?
|
|
301
|
+
isettings._persist(write_to_disk=_write_settings)
|
|
302
|
+
if _test:
|
|
303
|
+
return None
|
|
304
|
+
isettings._init_db()
|
|
305
|
+
load_from_isettings(
|
|
306
|
+
isettings, init=True, user=_user, write_settings=_write_settings
|
|
307
|
+
)
|
|
308
|
+
if _write_settings and isettings._is_cloud_sqlite:
|
|
309
|
+
isettings._cloud_sqlite_locker.lock()
|
|
310
|
+
logger.warning(
|
|
311
|
+
"locked instance (to unlock and push changes to the cloud SQLite file,"
|
|
312
|
+
" call: lamin disconnect)"
|
|
313
|
+
)
|
|
314
|
+
if register_on_hub and isettings.dialect != "sqlite":
|
|
315
|
+
from ._schema_metadata import update_schema_in_hub
|
|
316
|
+
|
|
317
|
+
update_schema_in_hub(access_token=access_token)
|
|
318
|
+
if _write_settings:
|
|
319
|
+
settings.auto_connect = True
|
|
320
|
+
except Exception as e:
|
|
321
|
+
from ._delete import delete_by_isettings
|
|
322
|
+
from .core._hub_core import delete_instance_record, delete_storage_record
|
|
323
|
+
|
|
324
|
+
if isettings is not None:
|
|
325
|
+
if _write_settings:
|
|
326
|
+
delete_by_isettings(isettings)
|
|
327
|
+
else:
|
|
328
|
+
settings._instance_settings = None
|
|
329
|
+
if (
|
|
330
|
+
user_handle != "anonymous" or access_token is not None
|
|
331
|
+
) and isettings.is_on_hub:
|
|
332
|
+
delete_instance_record(isettings._id, access_token=access_token)
|
|
333
|
+
if (
|
|
334
|
+
ssettings is not None
|
|
335
|
+
and (user_handle != "anonymous" or access_token is not None)
|
|
336
|
+
and ssettings.is_on_hub
|
|
337
|
+
):
|
|
338
|
+
delete_storage_record(ssettings._uuid, access_token=access_token) # type: ignore
|
|
339
|
+
raise e
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def load_from_isettings(
|
|
344
|
+
isettings: InstanceSettings,
|
|
345
|
+
*,
|
|
346
|
+
init: bool = False,
|
|
347
|
+
user: UserSettings | None = None,
|
|
348
|
+
write_settings: bool = True,
|
|
349
|
+
) -> None:
|
|
350
|
+
from .core._setup_bionty_sources import write_bionty_sources
|
|
351
|
+
|
|
352
|
+
user = settings.user if user is None else user
|
|
353
|
+
|
|
354
|
+
if init:
|
|
355
|
+
# during init both user and storage need to be registered
|
|
356
|
+
register_user_and_storage_in_instance(isettings, user)
|
|
357
|
+
write_bionty_sources(isettings)
|
|
358
|
+
isettings._update_cloud_sqlite_file(unlock_cloud_sqlite=False)
|
|
359
|
+
else:
|
|
360
|
+
# when loading, django is already set up
|
|
361
|
+
#
|
|
362
|
+
# only register user if the instance is connected
|
|
363
|
+
# for the first time in an environment
|
|
364
|
+
# this is our best proxy for that the user might not
|
|
365
|
+
# yet be registered
|
|
366
|
+
if not isettings._get_settings_file().exists():
|
|
367
|
+
register_user(user)
|
|
368
|
+
isettings._persist(write_to_disk=write_settings)
|
|
369
|
+
reload_lamindb(isettings)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def validate_sqlite_state(isettings: InstanceSettings) -> None:
|
|
373
|
+
if isettings._is_cloud_sqlite:
|
|
374
|
+
if (
|
|
375
|
+
# it's important to first evaluate the existence check
|
|
376
|
+
# for the local sqlite file because it doesn't need a network
|
|
377
|
+
# request
|
|
378
|
+
isettings._sqlite_file_local.exists()
|
|
379
|
+
and not isettings._sqlite_file.exists()
|
|
380
|
+
):
|
|
381
|
+
raise RuntimeError(
|
|
382
|
+
ERROR_SQLITE_CACHE.format(
|
|
383
|
+
isettings._sqlite_file, isettings._sqlite_file_local
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def infer_instance_name(
|
|
389
|
+
*,
|
|
390
|
+
storage: UPathStr,
|
|
391
|
+
name: str | None = None,
|
|
392
|
+
db: PostgresDsn | None = None,
|
|
393
|
+
) -> str:
|
|
394
|
+
if name is not None:
|
|
395
|
+
if "/" in name:
|
|
396
|
+
raise ValueError("Invalid instance name: '/' delimiter not allowed.")
|
|
397
|
+
return name
|
|
398
|
+
if db is not None:
|
|
399
|
+
logger.warning("using the sql database name for the instance name")
|
|
400
|
+
# this isn't a great way to access the db name
|
|
401
|
+
# could use LaminDsn instead
|
|
402
|
+
return str(db).split("/")[-1]
|
|
403
|
+
if storage == "create-s3":
|
|
404
|
+
raise ValueError("pass name to init if storage = 'create-s3'")
|
|
405
|
+
storage_path = UPath(storage)
|
|
406
|
+
if storage_path.name != "":
|
|
407
|
+
name = storage_path.name
|
|
408
|
+
else:
|
|
409
|
+
# dedicated treatment of bucket names
|
|
410
|
+
name = storage_path._url.netloc
|
|
411
|
+
return name.lower()
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.db import connection
|
|
4
|
+
from django.db.migrations.loader import MigrationLoader
|
|
5
|
+
from lamin_utils import logger
|
|
6
|
+
from packaging import version
|
|
7
|
+
|
|
8
|
+
from ._check_setup import _check_instance_setup
|
|
9
|
+
from .core._settings import settings
|
|
10
|
+
from .core.django import setup_django
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# for the django-based synching code, see laminhub_rest
|
|
14
|
+
def check_whether_migrations_in_sync(db_version_str: str):
|
|
15
|
+
from importlib import metadata
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
installed_version_str = metadata.version("lamindb")
|
|
19
|
+
except metadata.PackageNotFoundError:
|
|
20
|
+
return None
|
|
21
|
+
if db_version_str is None:
|
|
22
|
+
logger.warning("no lamindb version stored to compare with installed version")
|
|
23
|
+
return None
|
|
24
|
+
installed_version = version.parse(installed_version_str)
|
|
25
|
+
db_version = version.parse(db_version_str)
|
|
26
|
+
if (
|
|
27
|
+
installed_version.major < db_version.major
|
|
28
|
+
or installed_version.minor < db_version.minor
|
|
29
|
+
):
|
|
30
|
+
db_version_lower = f"{db_version.major}.{db_version.minor}"
|
|
31
|
+
db_version_upper = f"{db_version.major}.{db_version.minor + 1}"
|
|
32
|
+
logger.warning(
|
|
33
|
+
f"the database ({db_version_str}) is ahead of your installed lamindb"
|
|
34
|
+
f" package ({installed_version_str})"
|
|
35
|
+
)
|
|
36
|
+
logger.important(
|
|
37
|
+
"please update lamindb: pip install"
|
|
38
|
+
f' "lamindb>={db_version_lower},<{db_version_upper}"'
|
|
39
|
+
)
|
|
40
|
+
elif (
|
|
41
|
+
installed_version.major > db_version.major
|
|
42
|
+
or installed_version.minor > db_version.minor
|
|
43
|
+
):
|
|
44
|
+
logger.warning(
|
|
45
|
+
f"the database ({db_version_str}) is behind your installed lamindb package"
|
|
46
|
+
f" ({installed_version_str})"
|
|
47
|
+
)
|
|
48
|
+
logger.important("consider migrating your database: lamin migrate deploy")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# for tests, see lamin-cli
|
|
52
|
+
class migrate:
|
|
53
|
+
"""Manage migrations.
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
|
|
57
|
+
>>> import lamindb as ln
|
|
58
|
+
>>> ln.setup.migrate.create()
|
|
59
|
+
>>> ln.setup.migrate.deploy()
|
|
60
|
+
>>> ln.setup.migrate.check()
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def create(cls) -> None:
|
|
66
|
+
"""Create a migration."""
|
|
67
|
+
if _check_instance_setup():
|
|
68
|
+
raise RuntimeError("Restart Python session to create migration or use CLI!")
|
|
69
|
+
setup_django(settings.instance, create_migrations=True)
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def deploy(cls) -> None:
|
|
73
|
+
"""Deploy a migration."""
|
|
74
|
+
from ._schema_metadata import update_schema_in_hub
|
|
75
|
+
|
|
76
|
+
if _check_instance_setup():
|
|
77
|
+
raise RuntimeError("Restart Python session to migrate or use CLI!")
|
|
78
|
+
from lamindb_setup.core._hub_client import call_with_fallback_auth
|
|
79
|
+
from lamindb_setup.core._hub_crud import (
|
|
80
|
+
select_collaborator,
|
|
81
|
+
update_instance,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if settings.instance.is_on_hub:
|
|
85
|
+
# double check that user is an admin, otherwise will fail below
|
|
86
|
+
# due to insufficient SQL permissions with cryptic error
|
|
87
|
+
collaborator = call_with_fallback_auth(
|
|
88
|
+
select_collaborator,
|
|
89
|
+
instance_id=settings.instance._id,
|
|
90
|
+
account_id=settings.user._uuid,
|
|
91
|
+
)
|
|
92
|
+
if collaborator is None or collaborator["role"] != "admin":
|
|
93
|
+
raise SystemExit(
|
|
94
|
+
"❌ Only admins can deploy migrations, please ensure that you're an"
|
|
95
|
+
f" admin: https://lamin.ai/{settings.instance.slug}/settings"
|
|
96
|
+
)
|
|
97
|
+
# we need lamindb to be installed, otherwise we can't populate the version
|
|
98
|
+
# information in the hub
|
|
99
|
+
import lamindb
|
|
100
|
+
|
|
101
|
+
# this sets up django and deploys the migrations
|
|
102
|
+
setup_django(settings.instance, deploy_migrations=True)
|
|
103
|
+
# this populates the hub
|
|
104
|
+
if settings.instance.is_on_hub:
|
|
105
|
+
logger.important(f"updating lamindb version in hub: {lamindb.__version__}")
|
|
106
|
+
# TODO: integrate update of instance table within update_schema_in_hub & below
|
|
107
|
+
if settings.instance.dialect != "sqlite":
|
|
108
|
+
update_schema_in_hub()
|
|
109
|
+
call_with_fallback_auth(
|
|
110
|
+
update_instance,
|
|
111
|
+
instance_id=settings.instance._id.hex,
|
|
112
|
+
instance_fields={"lamindb_version": lamindb.__version__},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def check(cls) -> bool:
|
|
117
|
+
"""Check whether Registry definitions are in sync with migrations."""
|
|
118
|
+
from django.core.management import call_command
|
|
119
|
+
|
|
120
|
+
setup_django(settings.instance)
|
|
121
|
+
try:
|
|
122
|
+
call_command("makemigrations", check_changes=True)
|
|
123
|
+
except SystemExit:
|
|
124
|
+
logger.error(
|
|
125
|
+
"migrations are not in sync with ORMs, please create a migration: lamin"
|
|
126
|
+
" migrate create"
|
|
127
|
+
)
|
|
128
|
+
return False
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def squash(
|
|
133
|
+
cls, package_name, migration_nr, start_migration_nr: str | None = None
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Squash migrations."""
|
|
136
|
+
from django.core.management import call_command
|
|
137
|
+
|
|
138
|
+
setup_django(settings.instance)
|
|
139
|
+
if start_migration_nr is not None:
|
|
140
|
+
call_command(
|
|
141
|
+
"squashmigrations", package_name, start_migration_nr, migration_nr
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
call_command("squashmigrations", package_name, migration_nr)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def show(cls) -> None:
|
|
148
|
+
"""Show migrations."""
|
|
149
|
+
from django.core.management import call_command
|
|
150
|
+
|
|
151
|
+
setup_django(settings.instance)
|
|
152
|
+
call_command("showmigrations")
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def defined_migrations(cls, latest: bool = False):
|
|
156
|
+
from io import StringIO
|
|
157
|
+
|
|
158
|
+
from django.core.management import call_command
|
|
159
|
+
|
|
160
|
+
def parse_migration_output(output):
|
|
161
|
+
"""Parse the output of the showmigrations command to get migration names."""
|
|
162
|
+
lines = output.splitlines()
|
|
163
|
+
|
|
164
|
+
# Initialize an empty dict to store migration names of each module
|
|
165
|
+
migration_names = {}
|
|
166
|
+
|
|
167
|
+
# Process each line
|
|
168
|
+
for line in lines:
|
|
169
|
+
if " " not in line:
|
|
170
|
+
# CLI displays the module name in bold
|
|
171
|
+
name = line.strip().replace("\x1b[1m", "")
|
|
172
|
+
migration_names[name] = []
|
|
173
|
+
continue
|
|
174
|
+
# Strip whitespace and split the line into status and migration name
|
|
175
|
+
migration_name = line.strip().split("] ")[-1].split(" ")[0]
|
|
176
|
+
# The second part is the migration name
|
|
177
|
+
migration_names[name].append(migration_name)
|
|
178
|
+
|
|
179
|
+
return migration_names
|
|
180
|
+
|
|
181
|
+
out = StringIO()
|
|
182
|
+
call_command("showmigrations", stdout=out)
|
|
183
|
+
out.seek(0)
|
|
184
|
+
output = out.getvalue()
|
|
185
|
+
if latest:
|
|
186
|
+
return {k: v[-1] for k, v in parse_migration_output(output).items()}
|
|
187
|
+
else:
|
|
188
|
+
return parse_migration_output(output)
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def deployed_migrations(cls, latest: bool = False):
|
|
192
|
+
"""Get the list of deployed migrations from Migration table in DB."""
|
|
193
|
+
if latest:
|
|
194
|
+
latest_migrations = {}
|
|
195
|
+
with connection.cursor() as cursor:
|
|
196
|
+
# query to get the latest migration for each app that is not squashed
|
|
197
|
+
cursor.execute(
|
|
198
|
+
"""
|
|
199
|
+
SELECT app, name
|
|
200
|
+
FROM django_migrations
|
|
201
|
+
WHERE id IN (
|
|
202
|
+
SELECT MAX(id)
|
|
203
|
+
FROM django_migrations
|
|
204
|
+
WHERE name NOT LIKE '%%_squashed_%%'
|
|
205
|
+
GROUP BY app
|
|
206
|
+
)
|
|
207
|
+
"""
|
|
208
|
+
)
|
|
209
|
+
# fetch all the results
|
|
210
|
+
for app, name in cursor.fetchall():
|
|
211
|
+
latest_migrations[app] = name
|
|
212
|
+
|
|
213
|
+
return latest_migrations
|
|
214
|
+
else:
|
|
215
|
+
# Load all migrations using Django's migration loader
|
|
216
|
+
loader = MigrationLoader(connection)
|
|
217
|
+
squashed_replacements = set()
|
|
218
|
+
for _key, migration in loader.disk_migrations.items():
|
|
219
|
+
if hasattr(migration, "replaces"):
|
|
220
|
+
squashed_replacements.update(migration.replaces)
|
|
221
|
+
|
|
222
|
+
deployed_migrations: dict = {}
|
|
223
|
+
with connection.cursor() as cursor:
|
|
224
|
+
cursor.execute(
|
|
225
|
+
"""
|
|
226
|
+
SELECT app, name, deployed
|
|
227
|
+
FROM django_migrations
|
|
228
|
+
ORDER BY app, deployed DESC
|
|
229
|
+
"""
|
|
230
|
+
)
|
|
231
|
+
for app, name, _deployed in cursor.fetchall():
|
|
232
|
+
# skip migrations that are part of a squashed migration
|
|
233
|
+
if (app, name) in squashed_replacements:
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
if app not in deployed_migrations:
|
|
237
|
+
deployed_migrations[app] = []
|
|
238
|
+
deployed_migrations[app].append(name)
|
|
239
|
+
return deployed_migrations
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from lamin_utils import logger
|
|
4
|
+
|
|
5
|
+
from .core._settings import settings
|
|
6
|
+
from .core._settings_storage import base62
|
|
7
|
+
from .core.django import setup_django
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(_test: bool = False):
|
|
11
|
+
"""Register an instance on the hub."""
|
|
12
|
+
from ._check_setup import _check_instance_setup
|
|
13
|
+
from .core._hub_core import init_instance as init_instance_hub
|
|
14
|
+
from .core._hub_core import init_storage as init_storage_hub
|
|
15
|
+
|
|
16
|
+
logger.warning("""lamin register will be removed soon""")
|
|
17
|
+
|
|
18
|
+
isettings = settings.instance
|
|
19
|
+
if not _check_instance_setup() and not _test:
|
|
20
|
+
setup_django(isettings)
|
|
21
|
+
|
|
22
|
+
ssettings = settings.instance.storage
|
|
23
|
+
if ssettings._uid is None and _test:
|
|
24
|
+
# because django isn't up, we can't get it from the database
|
|
25
|
+
ssettings._uid = base62(8)
|
|
26
|
+
# cannot yet populate the instance id here
|
|
27
|
+
ssettings._instance_id = None
|
|
28
|
+
# flag auto_populate_instance can be removed once FK migration is over
|
|
29
|
+
init_storage_hub(ssettings, auto_populate_instance=False)
|
|
30
|
+
init_instance_hub(isettings)
|
|
31
|
+
isettings._is_on_hub = True
|
|
32
|
+
isettings._persist()
|
|
33
|
+
if isettings.dialect != "sqlite" and not _test:
|
|
34
|
+
from ._schema_metadata import update_schema_in_hub
|
|
35
|
+
|
|
36
|
+
update_schema_in_hub()
|