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