lamindb_setup 0.76.6__py2.py3-none-any.whl → 0.76.7__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.
Files changed (47) hide show
  1. lamindb_setup/__init__.py +1 -1
  2. lamindb_setup/_cache.py +34 -34
  3. lamindb_setup/_check.py +7 -7
  4. lamindb_setup/_check_setup.py +79 -79
  5. lamindb_setup/_close.py +35 -35
  6. lamindb_setup/_connect_instance.py +433 -433
  7. lamindb_setup/_delete.py +137 -137
  8. lamindb_setup/_django.py +41 -41
  9. lamindb_setup/_exportdb.py +68 -68
  10. lamindb_setup/_importdb.py +50 -50
  11. lamindb_setup/_init_instance.py +374 -374
  12. lamindb_setup/_migrate.py +239 -236
  13. lamindb_setup/_register_instance.py +36 -36
  14. lamindb_setup/_schema.py +27 -27
  15. lamindb_setup/_schema_metadata.py +391 -391
  16. lamindb_setup/_set_managed_storage.py +55 -55
  17. lamindb_setup/_setup_user.py +118 -118
  18. lamindb_setup/_silence_loggers.py +44 -44
  19. lamindb_setup/core/__init__.py +21 -21
  20. lamindb_setup/core/_aws_credentials.py +151 -151
  21. lamindb_setup/core/_aws_storage.py +48 -48
  22. lamindb_setup/core/_deprecated.py +55 -55
  23. lamindb_setup/core/_docs.py +14 -14
  24. lamindb_setup/core/_hub_client.py +164 -161
  25. lamindb_setup/core/_hub_core.py +473 -473
  26. lamindb_setup/core/_hub_crud.py +211 -211
  27. lamindb_setup/core/_hub_utils.py +109 -109
  28. lamindb_setup/core/_private_django_api.py +88 -88
  29. lamindb_setup/core/_settings.py +138 -138
  30. lamindb_setup/core/_settings_instance.py +461 -460
  31. lamindb_setup/core/_settings_load.py +100 -100
  32. lamindb_setup/core/_settings_save.py +81 -81
  33. lamindb_setup/core/_settings_storage.py +393 -393
  34. lamindb_setup/core/_settings_store.py +72 -72
  35. lamindb_setup/core/_settings_user.py +51 -51
  36. lamindb_setup/core/_setup_bionty_sources.py +99 -99
  37. lamindb_setup/core/cloud_sqlite_locker.py +232 -232
  38. lamindb_setup/core/django.py +113 -113
  39. lamindb_setup/core/exceptions.py +12 -12
  40. lamindb_setup/core/hashing.py +114 -114
  41. lamindb_setup/core/types.py +19 -19
  42. lamindb_setup/core/upath.py +779 -779
  43. {lamindb_setup-0.76.6.dist-info → lamindb_setup-0.76.7.dist-info}/METADATA +4 -3
  44. lamindb_setup-0.76.7.dist-info/RECORD +46 -0
  45. {lamindb_setup-0.76.6.dist-info → lamindb_setup-0.76.7.dist-info}/WHEEL +1 -1
  46. lamindb_setup-0.76.6.dist-info/RECORD +0 -46
  47. {lamindb_setup-0.76.6.dist-info → lamindb_setup-0.76.7.dist-info}/LICENSE +0 -0
@@ -1,433 +1,433 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- from typing import TYPE_CHECKING
5
- from uuid import UUID
6
-
7
- from lamin_utils import logger
8
-
9
- from ._check_setup import _check_instance_setup
10
- from ._close import close as close_instance
11
- from ._init_instance import MESSAGE_NO_MULTIPLE_INSTANCE, load_from_isettings
12
- from ._migrate import check_whether_migrations_in_sync
13
- from ._silence_loggers import silence_loggers
14
- from .core._hub_core import connect_instance as load_instance_from_hub
15
- from .core._hub_utils import (
16
- LaminDsn,
17
- LaminDsnModel,
18
- )
19
- from .core._settings import settings
20
- from .core._settings_instance import InstanceSettings
21
- from .core._settings_load import load_instance_settings
22
- from .core._settings_storage import StorageSettings
23
- from .core._settings_store import instance_settings_file, settings_dir
24
- from .core.cloud_sqlite_locker import unlock_cloud_sqlite_upon_exception
25
-
26
- if TYPE_CHECKING:
27
- from pathlib import Path
28
-
29
- from .core.types import UPathStr
30
-
31
- # this is for testing purposes only
32
- # set to True only to test failed load
33
- _TEST_FAILED_LOAD = False
34
-
35
-
36
- INSTANCE_NOT_FOUND_MESSAGE = (
37
- "'{owner}/{name}' not found:"
38
- " '{hub_result}'\nCheck your permissions:"
39
- " https://lamin.ai/{owner}/{name}"
40
- )
41
-
42
-
43
- class InstanceNotFoundError(SystemExit):
44
- pass
45
-
46
-
47
- def check_db_dsn_equal_up_to_credentials(db_dsn_hub, db_dsn_local):
48
- return (
49
- db_dsn_hub.scheme == db_dsn_local.scheme
50
- and db_dsn_hub.host == db_dsn_local.host
51
- and db_dsn_hub.database == db_dsn_local.database
52
- and db_dsn_hub.port == db_dsn_local.port
53
- )
54
-
55
-
56
- def update_db_using_local(
57
- hub_instance_result: dict[str, str],
58
- settings_file: Path,
59
- db: str | None = None,
60
- raise_permission_error=True,
61
- ) -> str | None:
62
- db_updated = None
63
- # check if postgres
64
- if hub_instance_result["db_scheme"] == "postgresql":
65
- db_dsn_hub = LaminDsnModel(db=hub_instance_result["db"])
66
- if db is not None:
67
- db_dsn_local = LaminDsnModel(db=db)
68
- else:
69
- # read directly from the environment
70
- if os.getenv("LAMINDB_INSTANCE_DB") is not None:
71
- logger.important("loading db URL from env variable LAMINDB_INSTANCE_DB")
72
- db_dsn_local = LaminDsnModel(db=os.getenv("LAMINDB_INSTANCE_DB"))
73
- # read from a cached settings file in case the hub result is only
74
- # read level or inexistent
75
- elif settings_file.exists() and (
76
- db_dsn_hub.db.user is None
77
- or (db_dsn_hub.db.user is not None and "read" in db_dsn_hub.db.user)
78
- ):
79
- isettings = load_instance_settings(settings_file)
80
- db_dsn_local = LaminDsnModel(db=isettings.db)
81
- else:
82
- # just take the default hub result and ensure there is actually a user
83
- if (
84
- db_dsn_hub.db.user == "none"
85
- and db_dsn_hub.db.password == "none"
86
- and raise_permission_error
87
- ):
88
- raise PermissionError(
89
- "No database access, please ask your admin to provide you with"
90
- " a DB URL and pass it via --db <db_url>"
91
- )
92
- db_dsn_local = db_dsn_hub
93
- if not check_db_dsn_equal_up_to_credentials(db_dsn_hub.db, db_dsn_local.db):
94
- raise ValueError(
95
- "The local differs from the hub database information:\n 1. did you"
96
- " pass a wrong db URL with --db?\n 2. did your database get updated by"
97
- " an admin?\nConsider deleting your cached database environment:\nrm"
98
- f" {settings_file.as_posix()}"
99
- )
100
- db_updated = LaminDsn.build(
101
- scheme=db_dsn_hub.db.scheme,
102
- user=db_dsn_local.db.user,
103
- password=db_dsn_local.db.password,
104
- host=db_dsn_hub.db.host, # type: ignore
105
- port=db_dsn_hub.db.port,
106
- database=db_dsn_hub.db.database,
107
- )
108
- return db_updated
109
-
110
-
111
- def _connect_instance(
112
- owner: str,
113
- name: str,
114
- *,
115
- db: str | None = None,
116
- raise_permission_error: bool = True,
117
- ) -> InstanceSettings:
118
- settings_file = instance_settings_file(name, owner)
119
- make_hub_request = True
120
- if settings_file.exists():
121
- isettings = load_instance_settings(settings_file)
122
- # skip hub request for a purely local instance
123
- make_hub_request = isettings.is_remote
124
- if make_hub_request:
125
- # the following will return a string if the instance does not exist
126
- # on the hub
127
- hub_result = load_instance_from_hub(owner=owner, name=name)
128
- # if hub_result is not a string, it means it made a request
129
- # that successfully returned metadata
130
- if not isinstance(hub_result, str):
131
- instance_result, storage_result = hub_result
132
- db_updated = update_db_using_local(
133
- instance_result,
134
- settings_file,
135
- db=db,
136
- raise_permission_error=raise_permission_error,
137
- )
138
- ssettings = StorageSettings(
139
- root=storage_result["root"],
140
- region=storage_result["region"],
141
- uid=storage_result["lnid"],
142
- uuid=UUID(storage_result["id"]),
143
- instance_id=UUID(instance_result["id"]),
144
- )
145
- isettings = InstanceSettings(
146
- id=UUID(instance_result["id"]),
147
- owner=owner,
148
- name=name,
149
- storage=ssettings,
150
- db=db_updated,
151
- schema=instance_result["schema_str"],
152
- git_repo=instance_result["git_repo"],
153
- keep_artifacts_local=bool(instance_result["keep_artifacts_local"]),
154
- is_on_hub=True,
155
- )
156
- check_whether_migrations_in_sync(instance_result["lamindb_version"])
157
- else:
158
- message = INSTANCE_NOT_FOUND_MESSAGE.format(
159
- owner=owner, name=name, hub_result=hub_result
160
- )
161
- if settings_file.exists():
162
- isettings = load_instance_settings(settings_file)
163
- if isettings.is_remote:
164
- raise InstanceNotFoundError(message)
165
- else:
166
- raise InstanceNotFoundError(message)
167
- return isettings
168
-
169
-
170
- @unlock_cloud_sqlite_upon_exception(ignore_prev_locker=True)
171
- def connect(
172
- slug: str,
173
- *,
174
- db: str | None = None,
175
- storage: UPathStr | None = None,
176
- _raise_not_found_error: bool = True,
177
- _test: bool = False,
178
- ) -> str | tuple | None:
179
- """Connect to instance.
180
-
181
- Args:
182
- slug: The instance slug `account_handle/instance_name` or URL.
183
- If the instance is owned by you, it suffices to pass the instance name.
184
- db: Load the instance with an updated database URL.
185
- storage: Load the instance with an updated default storage.
186
- """
187
- isettings: InstanceSettings = None # type: ignore
188
- try:
189
- owner, name = get_owner_name_from_identifier(slug)
190
-
191
- if _check_instance_setup() and not _test:
192
- if (
193
- settings._instance_exists
194
- and f"{owner}/{name}" == settings.instance.slug
195
- ):
196
- logger.info(f"connected lamindb: {settings.instance.slug}")
197
- return None
198
- else:
199
- raise RuntimeError(MESSAGE_NO_MULTIPLE_INSTANCE)
200
- elif settings._instance_exists and f"{owner}/{name}" != settings.instance.slug:
201
- close_instance(mute=True)
202
-
203
- try:
204
- isettings = _connect_instance(owner, name, db=db)
205
- except InstanceNotFoundError as e:
206
- if _raise_not_found_error:
207
- raise e
208
- else:
209
- return "instance-not-found"
210
- if isinstance(isettings, str):
211
- return isettings
212
- if storage is not None:
213
- update_isettings_with_storage(isettings, storage)
214
- isettings._persist()
215
- if _test:
216
- return None
217
- silence_loggers()
218
- check, msg = isettings._load_db()
219
- if not check:
220
- local_db = (
221
- isettings._is_cloud_sqlite and isettings._sqlite_file_local.exists()
222
- )
223
- if local_db:
224
- logger.warning(
225
- "SQLite file does not exist in the cloud, but exists locally:"
226
- f" {isettings._sqlite_file_local}\nTo push the file to the cloud,"
227
- " call: lamin close"
228
- )
229
- elif _raise_not_found_error:
230
- raise SystemExit(msg)
231
- else:
232
- logger.warning(
233
- f"instance exists with id {isettings._id.hex}, but database is not"
234
- " loadable: re-initializing"
235
- )
236
- return "instance-corrupted-or-deleted"
237
- # this is for testing purposes only
238
- if _TEST_FAILED_LOAD:
239
- raise RuntimeError("Technical testing error.")
240
-
241
- if storage is not None and isettings.dialect == "sqlite":
242
- update_root_field_in_default_storage(isettings)
243
- # below is for backfilling the instance_uid value
244
- # we'll enable it once more people migrated to 0.71.0
245
- # ssettings_record = isettings.storage.record
246
- # if ssettings_record.instance_uid is None:
247
- # ssettings_record.instance_uid = isettings.uid
248
- # # try saving if not read-only access
249
- # try:
250
- # ssettings_record.save()
251
- # # raised by django when the access is denied
252
- # except ProgrammingError:
253
- # pass
254
- load_from_isettings(isettings)
255
- except Exception as e:
256
- if isettings is not None:
257
- isettings._get_settings_file().unlink(missing_ok=True) # type: ignore
258
- raise e
259
- # rename lnschema_bionty to bionty for sql tables
260
- if "bionty" in isettings.schema:
261
- no_lnschema_bionty_file = (
262
- settings_dir / f"no_lnschema_bionty-{isettings.slug.replace('/', '')}"
263
- )
264
- if not no_lnschema_bionty_file.exists():
265
- migrate_lnschema_bionty(isettings, no_lnschema_bionty_file)
266
- return None
267
-
268
-
269
- def migrate_lnschema_bionty(isettings: InstanceSettings, no_lnschema_bionty_file: Path):
270
- """Migrate lnschema_bionty tables to bionty tables if bionty_source doesn't exist.
271
-
272
- :param db_uri: str, database URI (e.g., 'sqlite:///path/to/db.sqlite' or 'postgresql://user:password@host:port/dbname')
273
- """
274
- from urllib.parse import urlparse
275
-
276
- parsed_uri = urlparse(isettings.db)
277
- db_type = parsed_uri.scheme
278
-
279
- if db_type == "sqlite":
280
- import sqlite3
281
-
282
- conn = sqlite3.connect(parsed_uri.path)
283
- elif db_type in ["postgresql", "postgres"]:
284
- import psycopg2
285
-
286
- conn = psycopg2.connect(isettings.db)
287
- else:
288
- raise ValueError("Unsupported database type. Use 'sqlite' or 'postgresql' URI.")
289
-
290
- cur = conn.cursor()
291
-
292
- try:
293
- # check if bionty_source table exists
294
- if db_type == "sqlite":
295
- cur.execute(
296
- "SELECT name FROM sqlite_master WHERE type='table' AND name='bionty_source'"
297
- )
298
- migrated = cur.fetchone() is not None
299
-
300
- # tables that need to be renamed
301
- cur.execute(
302
- "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'lnschema_bionty_%'"
303
- )
304
- tables_to_rename = [
305
- row[0][len("lnschema_bionty_") :] for row in cur.fetchall()
306
- ]
307
- else: # postgres
308
- cur.execute(
309
- "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'bionty_source')"
310
- )
311
- migrated = cur.fetchone()[0]
312
-
313
- # tables that need to be renamed
314
- cur.execute(
315
- "SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'lnschema_bionty_%'"
316
- )
317
- tables_to_rename = [
318
- row[0][len("lnschema_bionty_") :] for row in cur.fetchall()
319
- ]
320
-
321
- if migrated:
322
- no_lnschema_bionty_file.touch(exist_ok=True)
323
- else:
324
- try:
325
- # rename tables only if bionty_source doesn't exist and there are tables to rename
326
- for table in tables_to_rename:
327
- if db_type == "sqlite":
328
- cur.execute(
329
- f"ALTER TABLE lnschema_bionty_{table} RENAME TO bionty_{table}"
330
- )
331
- else: # postgres
332
- cur.execute(
333
- f"ALTER TABLE lnschema_bionty_{table} RENAME TO bionty_{table};"
334
- )
335
-
336
- # update django_migrations table
337
- cur.execute(
338
- "UPDATE django_migrations SET app = 'bionty' WHERE app = 'lnschema_bionty'"
339
- )
340
-
341
- logger.warning(
342
- "Please uninstall lnschema-bionty via `pip uninstall lnschema-bionty`!"
343
- )
344
-
345
- no_lnschema_bionty_file.touch(exist_ok=True)
346
- except Exception:
347
- # read-only users can't rename tables
348
- pass
349
-
350
- conn.commit()
351
-
352
- except Exception as e:
353
- print(f"An error occurred: {e}")
354
- conn.rollback()
355
-
356
- finally:
357
- # close the cursor and connection
358
- cur.close()
359
- conn.close()
360
-
361
-
362
- def load(
363
- slug: str,
364
- *,
365
- db: str | None = None,
366
- storage: UPathStr | None = None,
367
- ) -> str | tuple | None:
368
- """Connect to instance and set ``auto-connect`` to true.
369
-
370
- This is exactly the same as ``ln.connect()`` except for that
371
- ``ln.connect()`` doesn't change the state of ``auto-connect``.
372
- """
373
- result = connect(slug, db=db, storage=storage)
374
- settings.auto_connect = True
375
- return result
376
-
377
-
378
- def get_owner_name_from_identifier(identifier: str):
379
- if "/" in identifier:
380
- if identifier.startswith("https://lamin.ai/"):
381
- identifier = identifier.replace("https://lamin.ai/", "")
382
- split = identifier.split("/")
383
- if len(split) > 2:
384
- raise ValueError(
385
- "The instance identifier needs to be 'owner/name', the instance name"
386
- " (owner is current user) or the URL: https://lamin.ai/owner/name."
387
- )
388
- owner, name = split
389
- else:
390
- owner = settings.user.handle
391
- name = identifier
392
- return owner, name
393
-
394
-
395
- def update_isettings_with_storage(
396
- isettings: InstanceSettings, storage: UPathStr
397
- ) -> None:
398
- ssettings = StorageSettings(storage)
399
- if ssettings.type_is_cloud:
400
- try: # triggering ssettings.id makes a lookup in the storage table
401
- logger.success(f"loaded storage: {ssettings.id} / {ssettings.root_as_str}")
402
- except RuntimeError as e:
403
- logger.error(
404
- "storage not registered!\n"
405
- "load instance without the `storage` arg and register storage root: "
406
- f"`lamin set storage --storage {storage}`"
407
- )
408
- raise e
409
- else:
410
- # local storage
411
- # assumption is you want to merely update the storage location
412
- isettings._storage = ssettings # need this here already
413
- # update isettings in place
414
- isettings._storage = ssettings
415
-
416
-
417
- # this is different from register!
418
- # register registers a new storage location
419
- # update_root_field_in_default_storage updates the root
420
- # field in the default storage locations
421
- def update_root_field_in_default_storage(isettings: InstanceSettings):
422
- from lnschema_core.models import Storage
423
-
424
- storages = Storage.objects.all()
425
- if len(storages) != 1:
426
- raise RuntimeError(
427
- "You have several storage locations: Can't identify in which storage"
428
- " location the root column is to be updated!"
429
- )
430
- storage = storages[0]
431
- storage.root = isettings.storage.root_as_str
432
- storage.save()
433
- logger.save(f"updated storage root {storage.id} to {isettings.storage.root_as_str}")
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import TYPE_CHECKING
5
+ from uuid import UUID
6
+
7
+ from lamin_utils import logger
8
+
9
+ from ._check_setup import _check_instance_setup
10
+ from ._close import close as close_instance
11
+ from ._init_instance import MESSAGE_NO_MULTIPLE_INSTANCE, load_from_isettings
12
+ from ._migrate import check_whether_migrations_in_sync
13
+ from ._silence_loggers import silence_loggers
14
+ from .core._hub_core import connect_instance as load_instance_from_hub
15
+ from .core._hub_utils import (
16
+ LaminDsn,
17
+ LaminDsnModel,
18
+ )
19
+ from .core._settings import settings
20
+ from .core._settings_instance import InstanceSettings
21
+ from .core._settings_load import load_instance_settings
22
+ from .core._settings_storage import StorageSettings
23
+ from .core._settings_store import instance_settings_file, settings_dir
24
+ from .core.cloud_sqlite_locker import unlock_cloud_sqlite_upon_exception
25
+
26
+ if TYPE_CHECKING:
27
+ from pathlib import Path
28
+
29
+ from .core.types import UPathStr
30
+
31
+ # this is for testing purposes only
32
+ # set to True only to test failed load
33
+ _TEST_FAILED_LOAD = False
34
+
35
+
36
+ INSTANCE_NOT_FOUND_MESSAGE = (
37
+ "'{owner}/{name}' not found:"
38
+ " '{hub_result}'\nCheck your permissions:"
39
+ " https://lamin.ai/{owner}/{name}"
40
+ )
41
+
42
+
43
+ class InstanceNotFoundError(SystemExit):
44
+ pass
45
+
46
+
47
+ def check_db_dsn_equal_up_to_credentials(db_dsn_hub, db_dsn_local):
48
+ return (
49
+ db_dsn_hub.scheme == db_dsn_local.scheme
50
+ and db_dsn_hub.host == db_dsn_local.host
51
+ and db_dsn_hub.database == db_dsn_local.database
52
+ and db_dsn_hub.port == db_dsn_local.port
53
+ )
54
+
55
+
56
+ def update_db_using_local(
57
+ hub_instance_result: dict[str, str],
58
+ settings_file: Path,
59
+ db: str | None = None,
60
+ raise_permission_error=True,
61
+ ) -> str | None:
62
+ db_updated = None
63
+ # check if postgres
64
+ if hub_instance_result["db_scheme"] == "postgresql":
65
+ db_dsn_hub = LaminDsnModel(db=hub_instance_result["db"])
66
+ if db is not None:
67
+ db_dsn_local = LaminDsnModel(db=db)
68
+ else:
69
+ # read directly from the environment
70
+ if os.getenv("LAMINDB_INSTANCE_DB") is not None:
71
+ logger.important("loading db URL from env variable LAMINDB_INSTANCE_DB")
72
+ db_dsn_local = LaminDsnModel(db=os.getenv("LAMINDB_INSTANCE_DB"))
73
+ # read from a cached settings file in case the hub result is only
74
+ # read level or inexistent
75
+ elif settings_file.exists() and (
76
+ db_dsn_hub.db.user is None
77
+ or (db_dsn_hub.db.user is not None and "read" in db_dsn_hub.db.user)
78
+ ):
79
+ isettings = load_instance_settings(settings_file)
80
+ db_dsn_local = LaminDsnModel(db=isettings.db)
81
+ else:
82
+ # just take the default hub result and ensure there is actually a user
83
+ if (
84
+ db_dsn_hub.db.user == "none"
85
+ and db_dsn_hub.db.password == "none"
86
+ and raise_permission_error
87
+ ):
88
+ raise PermissionError(
89
+ "No database access, please ask your admin to provide you with"
90
+ " a DB URL and pass it via --db <db_url>"
91
+ )
92
+ db_dsn_local = db_dsn_hub
93
+ if not check_db_dsn_equal_up_to_credentials(db_dsn_hub.db, db_dsn_local.db):
94
+ raise ValueError(
95
+ "The local differs from the hub database information:\n 1. did you"
96
+ " pass a wrong db URL with --db?\n 2. did your database get updated by"
97
+ " an admin?\nConsider deleting your cached database environment:\nrm"
98
+ f" {settings_file.as_posix()}"
99
+ )
100
+ db_updated = LaminDsn.build(
101
+ scheme=db_dsn_hub.db.scheme,
102
+ user=db_dsn_local.db.user,
103
+ password=db_dsn_local.db.password,
104
+ host=db_dsn_hub.db.host, # type: ignore
105
+ port=db_dsn_hub.db.port,
106
+ database=db_dsn_hub.db.database,
107
+ )
108
+ return db_updated
109
+
110
+
111
+ def _connect_instance(
112
+ owner: str,
113
+ name: str,
114
+ *,
115
+ db: str | None = None,
116
+ raise_permission_error: bool = True,
117
+ ) -> InstanceSettings:
118
+ settings_file = instance_settings_file(name, owner)
119
+ make_hub_request = True
120
+ if settings_file.exists():
121
+ isettings = load_instance_settings(settings_file)
122
+ # skip hub request for a purely local instance
123
+ make_hub_request = isettings.is_remote
124
+ if make_hub_request:
125
+ # the following will return a string if the instance does not exist
126
+ # on the hub
127
+ hub_result = load_instance_from_hub(owner=owner, name=name)
128
+ # if hub_result is not a string, it means it made a request
129
+ # that successfully returned metadata
130
+ if not isinstance(hub_result, str):
131
+ instance_result, storage_result = hub_result
132
+ db_updated = update_db_using_local(
133
+ instance_result,
134
+ settings_file,
135
+ db=db,
136
+ raise_permission_error=raise_permission_error,
137
+ )
138
+ ssettings = StorageSettings(
139
+ root=storage_result["root"],
140
+ region=storage_result["region"],
141
+ uid=storage_result["lnid"],
142
+ uuid=UUID(storage_result["id"]),
143
+ instance_id=UUID(instance_result["id"]),
144
+ )
145
+ isettings = InstanceSettings(
146
+ id=UUID(instance_result["id"]),
147
+ owner=owner,
148
+ name=name,
149
+ storage=ssettings,
150
+ db=db_updated,
151
+ schema=instance_result["schema_str"],
152
+ git_repo=instance_result["git_repo"],
153
+ keep_artifacts_local=bool(instance_result["keep_artifacts_local"]),
154
+ is_on_hub=True,
155
+ )
156
+ check_whether_migrations_in_sync(instance_result["lamindb_version"])
157
+ else:
158
+ message = INSTANCE_NOT_FOUND_MESSAGE.format(
159
+ owner=owner, name=name, hub_result=hub_result
160
+ )
161
+ if settings_file.exists():
162
+ isettings = load_instance_settings(settings_file)
163
+ if isettings.is_remote:
164
+ raise InstanceNotFoundError(message)
165
+ else:
166
+ raise InstanceNotFoundError(message)
167
+ return isettings
168
+
169
+
170
+ @unlock_cloud_sqlite_upon_exception(ignore_prev_locker=True)
171
+ def connect(
172
+ slug: str,
173
+ *,
174
+ db: str | None = None,
175
+ storage: UPathStr | None = None,
176
+ _raise_not_found_error: bool = True,
177
+ _test: bool = False,
178
+ ) -> str | tuple | None:
179
+ """Connect to instance.
180
+
181
+ Args:
182
+ slug: The instance slug `account_handle/instance_name` or URL.
183
+ If the instance is owned by you, it suffices to pass the instance name.
184
+ db: Load the instance with an updated database URL.
185
+ storage: Load the instance with an updated default storage.
186
+ """
187
+ isettings: InstanceSettings = None # type: ignore
188
+ try:
189
+ owner, name = get_owner_name_from_identifier(slug)
190
+
191
+ if _check_instance_setup() and not _test:
192
+ if (
193
+ settings._instance_exists
194
+ and f"{owner}/{name}" == settings.instance.slug
195
+ ):
196
+ logger.info(f"connected lamindb: {settings.instance.slug}")
197
+ return None
198
+ else:
199
+ raise RuntimeError(MESSAGE_NO_MULTIPLE_INSTANCE)
200
+ elif settings._instance_exists and f"{owner}/{name}" != settings.instance.slug:
201
+ close_instance(mute=True)
202
+
203
+ try:
204
+ isettings = _connect_instance(owner, name, db=db)
205
+ except InstanceNotFoundError as e:
206
+ if _raise_not_found_error:
207
+ raise e
208
+ else:
209
+ return "instance-not-found"
210
+ if isinstance(isettings, str):
211
+ return isettings
212
+ if storage is not None:
213
+ update_isettings_with_storage(isettings, storage)
214
+ isettings._persist()
215
+ if _test:
216
+ return None
217
+ silence_loggers()
218
+ check, msg = isettings._load_db()
219
+ if not check:
220
+ local_db = (
221
+ isettings._is_cloud_sqlite and isettings._sqlite_file_local.exists()
222
+ )
223
+ if local_db:
224
+ logger.warning(
225
+ "SQLite file does not exist in the cloud, but exists locally:"
226
+ f" {isettings._sqlite_file_local}\nTo push the file to the cloud,"
227
+ " call: lamin close"
228
+ )
229
+ elif _raise_not_found_error:
230
+ raise SystemExit(msg)
231
+ else:
232
+ logger.warning(
233
+ f"instance exists with id {isettings._id.hex}, but database is not"
234
+ " loadable: re-initializing"
235
+ )
236
+ return "instance-corrupted-or-deleted"
237
+ # this is for testing purposes only
238
+ if _TEST_FAILED_LOAD:
239
+ raise RuntimeError("Technical testing error.")
240
+
241
+ if storage is not None and isettings.dialect == "sqlite":
242
+ update_root_field_in_default_storage(isettings)
243
+ # below is for backfilling the instance_uid value
244
+ # we'll enable it once more people migrated to 0.71.0
245
+ # ssettings_record = isettings.storage.record
246
+ # if ssettings_record.instance_uid is None:
247
+ # ssettings_record.instance_uid = isettings.uid
248
+ # # try saving if not read-only access
249
+ # try:
250
+ # ssettings_record.save()
251
+ # # raised by django when the access is denied
252
+ # except ProgrammingError:
253
+ # pass
254
+ load_from_isettings(isettings)
255
+ except Exception as e:
256
+ if isettings is not None:
257
+ isettings._get_settings_file().unlink(missing_ok=True) # type: ignore
258
+ raise e
259
+ # rename lnschema_bionty to bionty for sql tables
260
+ if "bionty" in isettings.schema:
261
+ no_lnschema_bionty_file = (
262
+ settings_dir / f"no_lnschema_bionty-{isettings.slug.replace('/', '')}"
263
+ )
264
+ if not no_lnschema_bionty_file.exists():
265
+ migrate_lnschema_bionty(isettings, no_lnschema_bionty_file)
266
+ return None
267
+
268
+
269
+ def migrate_lnschema_bionty(isettings: InstanceSettings, no_lnschema_bionty_file: Path):
270
+ """Migrate lnschema_bionty tables to bionty tables if bionty_source doesn't exist.
271
+
272
+ :param db_uri: str, database URI (e.g., 'sqlite:///path/to/db.sqlite' or 'postgresql://user:password@host:port/dbname')
273
+ """
274
+ from urllib.parse import urlparse
275
+
276
+ parsed_uri = urlparse(isettings.db)
277
+ db_type = parsed_uri.scheme
278
+
279
+ if db_type == "sqlite":
280
+ import sqlite3
281
+
282
+ conn = sqlite3.connect(parsed_uri.path)
283
+ elif db_type in ["postgresql", "postgres"]:
284
+ import psycopg2
285
+
286
+ conn = psycopg2.connect(isettings.db)
287
+ else:
288
+ raise ValueError("Unsupported database type. Use 'sqlite' or 'postgresql' URI.")
289
+
290
+ cur = conn.cursor()
291
+
292
+ try:
293
+ # check if bionty_source table exists
294
+ if db_type == "sqlite":
295
+ cur.execute(
296
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='bionty_source'"
297
+ )
298
+ migrated = cur.fetchone() is not None
299
+
300
+ # tables that need to be renamed
301
+ cur.execute(
302
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'lnschema_bionty_%'"
303
+ )
304
+ tables_to_rename = [
305
+ row[0][len("lnschema_bionty_") :] for row in cur.fetchall()
306
+ ]
307
+ else: # postgres
308
+ cur.execute(
309
+ "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'bionty_source')"
310
+ )
311
+ migrated = cur.fetchone()[0]
312
+
313
+ # tables that need to be renamed
314
+ cur.execute(
315
+ "SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'lnschema_bionty_%'"
316
+ )
317
+ tables_to_rename = [
318
+ row[0][len("lnschema_bionty_") :] for row in cur.fetchall()
319
+ ]
320
+
321
+ if migrated:
322
+ no_lnschema_bionty_file.touch(exist_ok=True)
323
+ else:
324
+ try:
325
+ # rename tables only if bionty_source doesn't exist and there are tables to rename
326
+ for table in tables_to_rename:
327
+ if db_type == "sqlite":
328
+ cur.execute(
329
+ f"ALTER TABLE lnschema_bionty_{table} RENAME TO bionty_{table}"
330
+ )
331
+ else: # postgres
332
+ cur.execute(
333
+ f"ALTER TABLE lnschema_bionty_{table} RENAME TO bionty_{table};"
334
+ )
335
+
336
+ # update django_migrations table
337
+ cur.execute(
338
+ "UPDATE django_migrations SET app = 'bionty' WHERE app = 'lnschema_bionty'"
339
+ )
340
+
341
+ logger.warning(
342
+ "Please uninstall lnschema-bionty via `pip uninstall lnschema-bionty`!"
343
+ )
344
+
345
+ no_lnschema_bionty_file.touch(exist_ok=True)
346
+ except Exception:
347
+ # read-only users can't rename tables
348
+ pass
349
+
350
+ conn.commit()
351
+
352
+ except Exception as e:
353
+ print(f"An error occurred: {e}")
354
+ conn.rollback()
355
+
356
+ finally:
357
+ # close the cursor and connection
358
+ cur.close()
359
+ conn.close()
360
+
361
+
362
+ def load(
363
+ slug: str,
364
+ *,
365
+ db: str | None = None,
366
+ storage: UPathStr | None = None,
367
+ ) -> str | tuple | None:
368
+ """Connect to instance and set ``auto-connect`` to true.
369
+
370
+ This is exactly the same as ``ln.connect()`` except for that
371
+ ``ln.connect()`` doesn't change the state of ``auto-connect``.
372
+ """
373
+ result = connect(slug, db=db, storage=storage)
374
+ settings.auto_connect = True
375
+ return result
376
+
377
+
378
+ def get_owner_name_from_identifier(identifier: str):
379
+ if "/" in identifier:
380
+ if identifier.startswith("https://lamin.ai/"):
381
+ identifier = identifier.replace("https://lamin.ai/", "")
382
+ split = identifier.split("/")
383
+ if len(split) > 2:
384
+ raise ValueError(
385
+ "The instance identifier needs to be 'owner/name', the instance name"
386
+ " (owner is current user) or the URL: https://lamin.ai/owner/name."
387
+ )
388
+ owner, name = split
389
+ else:
390
+ owner = settings.user.handle
391
+ name = identifier
392
+ return owner, name
393
+
394
+
395
+ def update_isettings_with_storage(
396
+ isettings: InstanceSettings, storage: UPathStr
397
+ ) -> None:
398
+ ssettings = StorageSettings(storage)
399
+ if ssettings.type_is_cloud:
400
+ try: # triggering ssettings.id makes a lookup in the storage table
401
+ logger.success(f"loaded storage: {ssettings.id} / {ssettings.root_as_str}")
402
+ except RuntimeError as e:
403
+ logger.error(
404
+ "storage not registered!\n"
405
+ "load instance without the `storage` arg and register storage root: "
406
+ f"`lamin set storage --storage {storage}`"
407
+ )
408
+ raise e
409
+ else:
410
+ # local storage
411
+ # assumption is you want to merely update the storage location
412
+ isettings._storage = ssettings # need this here already
413
+ # update isettings in place
414
+ isettings._storage = ssettings
415
+
416
+
417
+ # this is different from register!
418
+ # register registers a new storage location
419
+ # update_root_field_in_default_storage updates the root
420
+ # field in the default storage locations
421
+ def update_root_field_in_default_storage(isettings: InstanceSettings):
422
+ from lnschema_core.models import Storage
423
+
424
+ storages = Storage.objects.all()
425
+ if len(storages) != 1:
426
+ raise RuntimeError(
427
+ "You have several storage locations: Can't identify in which storage"
428
+ " location the root column is to be updated!"
429
+ )
430
+ storage = storages[0]
431
+ storage.root = isettings.storage.root_as_str
432
+ storage.save()
433
+ logger.save(f"updated storage root {storage.id} to {isettings.storage.root_as_str}")