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.
Files changed (47) hide show
  1. lamindb_setup/__init__.py +74 -0
  2. lamindb_setup/_cache.py +48 -0
  3. lamindb_setup/_check.py +7 -0
  4. lamindb_setup/_check_setup.py +92 -0
  5. lamindb_setup/_close.py +35 -0
  6. lamindb_setup/_connect_instance.py +429 -0
  7. lamindb_setup/_delete.py +141 -0
  8. lamindb_setup/_django.py +39 -0
  9. lamindb_setup/_entry_points.py +22 -0
  10. lamindb_setup/_exportdb.py +68 -0
  11. lamindb_setup/_importdb.py +50 -0
  12. lamindb_setup/_init_instance.py +411 -0
  13. lamindb_setup/_migrate.py +239 -0
  14. lamindb_setup/_register_instance.py +36 -0
  15. lamindb_setup/_schema.py +27 -0
  16. lamindb_setup/_schema_metadata.py +411 -0
  17. lamindb_setup/_set_managed_storage.py +55 -0
  18. lamindb_setup/_setup_user.py +137 -0
  19. lamindb_setup/_silence_loggers.py +44 -0
  20. lamindb_setup/core/__init__.py +21 -0
  21. lamindb_setup/core/_aws_credentials.py +151 -0
  22. lamindb_setup/core/_aws_storage.py +48 -0
  23. lamindb_setup/core/_deprecated.py +55 -0
  24. lamindb_setup/core/_docs.py +14 -0
  25. lamindb_setup/core/_hub_client.py +173 -0
  26. lamindb_setup/core/_hub_core.py +554 -0
  27. lamindb_setup/core/_hub_crud.py +211 -0
  28. lamindb_setup/core/_hub_utils.py +109 -0
  29. lamindb_setup/core/_private_django_api.py +88 -0
  30. lamindb_setup/core/_settings.py +184 -0
  31. lamindb_setup/core/_settings_instance.py +485 -0
  32. lamindb_setup/core/_settings_load.py +117 -0
  33. lamindb_setup/core/_settings_save.py +92 -0
  34. lamindb_setup/core/_settings_storage.py +350 -0
  35. lamindb_setup/core/_settings_store.py +75 -0
  36. lamindb_setup/core/_settings_user.py +55 -0
  37. lamindb_setup/core/_setup_bionty_sources.py +101 -0
  38. lamindb_setup/core/cloud_sqlite_locker.py +237 -0
  39. lamindb_setup/core/django.py +115 -0
  40. lamindb_setup/core/exceptions.py +10 -0
  41. lamindb_setup/core/hashing.py +116 -0
  42. lamindb_setup/core/types.py +17 -0
  43. lamindb_setup/core/upath.py +779 -0
  44. lamindb_setup-0.78.0.dist-info/LICENSE +201 -0
  45. lamindb_setup-0.78.0.dist-info/METADATA +47 -0
  46. lamindb_setup-0.78.0.dist-info/RECORD +47 -0
  47. lamindb_setup-0.78.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,74 @@
1
+ """Setup & configure LaminDB.
2
+
3
+ Many functions in this "setup API" have a matching command in the :doc:`docs:cli` CLI.
4
+
5
+ Guide: :doc:`docs:setup`.
6
+
7
+ Basic operations:
8
+
9
+ .. autosummary::
10
+ :toctree:
11
+
12
+ login
13
+ logout
14
+ init
15
+ close
16
+ delete
17
+
18
+ Instance operations:
19
+
20
+ .. autosummary::
21
+ :toctree:
22
+
23
+ migrate
24
+
25
+ Modules & settings:
26
+
27
+ .. autosummary::
28
+ :toctree:
29
+
30
+ settings
31
+ core
32
+ django
33
+
34
+ """
35
+
36
+ __version__ = "0.78.0" # denote a release candidate for 0.1.0 with 0.1rc1
37
+
38
+ import os as _os
39
+ import sys as _sys
40
+
41
+ from . import core
42
+ from ._check_setup import _check_instance_setup
43
+ from ._close import close
44
+ from ._connect_instance import connect, load
45
+ from ._delete import delete
46
+ from ._django import django
47
+ from ._entry_points import call_registered_entry_points as _call_registered_entry_points
48
+ from ._init_instance import init
49
+ from ._migrate import migrate
50
+ from ._register_instance import register
51
+ from ._setup_user import login, logout
52
+ from .core._settings import settings
53
+
54
+ _TESTING = _os.getenv("LAMIN_TESTING") is not None
55
+
56
+ # hide the supabase error in a thread on windows
57
+ if _os.name == "nt":
58
+ if _sys.version_info.minor > 7:
59
+ import threading
60
+
61
+ _original_excepthook = threading.excepthook
62
+
63
+ def _except_hook(args):
64
+ is_overflow = args.exc_type is OverflowError
65
+ for_timeout = str(args.exc_value) == "timeout value is too large"
66
+ if not (is_overflow and for_timeout):
67
+ _original_excepthook(args)
68
+
69
+ threading.excepthook = _except_hook
70
+
71
+ # provide a way for other packages to run custom code on import
72
+ _call_registered_entry_points("lamindb_setup.on_import")
73
+
74
+ settings.__doc__ = """Global :class:`~lamindb.setup.core.SetupSettings`."""
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+
5
+ from lamin_utils import logger
6
+
7
+ from .core._settings_save import save_system_storage_settings
8
+
9
+
10
+ def clear_cache_dir():
11
+ from lamindb_setup import close, settings
12
+
13
+ if settings.instance._is_cloud_sqlite:
14
+ logger.warning(
15
+ "Closing the current instance to update the cloud sqlite database."
16
+ )
17
+ close()
18
+
19
+ cache_dir = settings.cache_dir
20
+ shutil.rmtree(cache_dir)
21
+ cache_dir.mkdir()
22
+ logger.success("The cache directory was cleared.")
23
+
24
+
25
+ def get_cache_dir():
26
+ from lamindb_setup import settings
27
+
28
+ return settings.cache_dir.as_posix()
29
+
30
+
31
+ def set_cache_dir(cache_dir: str):
32
+ from lamindb_setup.core._settings import (
33
+ DEFAULT_CACHE_DIR,
34
+ _process_cache_path,
35
+ settings,
36
+ )
37
+
38
+ old_cache_dir = settings.cache_dir
39
+ new_cache_dir = _process_cache_path(cache_dir)
40
+ if new_cache_dir is None:
41
+ new_cache_dir = DEFAULT_CACHE_DIR
42
+ if new_cache_dir != old_cache_dir:
43
+ shutil.copytree(old_cache_dir, new_cache_dir, dirs_exist_ok=True)
44
+ shutil.rmtree(old_cache_dir)
45
+ logger.info("The current cache directory was moved to the specified location")
46
+ new_cache_dir = new_cache_dir.resolve()
47
+ save_system_storage_settings(new_cache_dir)
48
+ settings._cache_dir = new_cache_dir
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def check():
5
+ from django.core.management import call_command
6
+
7
+ call_command("check")
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib as il
4
+ import os
5
+ from typing import TYPE_CHECKING
6
+
7
+ from lamin_utils import logger
8
+
9
+ from ._silence_loggers import silence_loggers
10
+ from .core import django
11
+ from .core._settings import settings
12
+ from .core._settings_store import current_instance_settings_file
13
+ from .core.exceptions import DefaultMessageException
14
+
15
+ if TYPE_CHECKING:
16
+ from .core._settings_instance import InstanceSettings
17
+
18
+
19
+ class InstanceNotSetupError(DefaultMessageException):
20
+ default_message = """\
21
+ To use lamindb, you need to connect to an instance.
22
+
23
+ Connect to an instance: `ln.connect()`. Init an instance: `ln.setup.init()`.
24
+
25
+ If you used the CLI to set up lamindb in a notebook, restart the Python session.
26
+ """
27
+
28
+
29
+ CURRENT_ISETTINGS: InstanceSettings | None = None
30
+
31
+
32
+ def _get_current_instance_settings() -> InstanceSettings | None:
33
+ global CURRENT_ISETTINGS
34
+
35
+ if CURRENT_ISETTINGS is not None:
36
+ return CURRENT_ISETTINGS
37
+ if current_instance_settings_file().exists():
38
+ from .core._settings_load import load_instance_settings
39
+
40
+ try:
41
+ isettings = load_instance_settings()
42
+ except Exception as e:
43
+ # user will get more detailed traceback once they run the CLI
44
+ logger.error(
45
+ "Current instance cannot be reached, disconnect from it: `lamin disconnect`\n"
46
+ "Alternatively, init or load a connectable instance on the"
47
+ " command line: `lamin connect <instance>` or `lamin init <...>`"
48
+ )
49
+ raise e
50
+ return isettings
51
+ else:
52
+ return None
53
+
54
+
55
+ # we make this a private function because in all the places it's used,
56
+ # users should not see it
57
+ def _check_instance_setup(
58
+ from_lamindb: bool = False, from_module: str | None = None
59
+ ) -> bool:
60
+ reload_module = from_lamindb or from_module is not None
61
+ from ._init_instance import get_schema_module_name, reload_schema_modules
62
+
63
+ if django.IS_SETUP:
64
+ # reload logic here because module might not yet have been imported
65
+ # upon first setup
66
+ if from_module is not None:
67
+ il.reload(il.import_module(from_module))
68
+ return True
69
+ silence_loggers()
70
+ if os.environ.get("LAMINDB_MULTI_INSTANCE") == "true":
71
+ logger.warning(
72
+ "running LaminDB in multi-instance mode; you'll experience "
73
+ "errors in regular lamindb usage"
74
+ )
75
+ return True
76
+ isettings = _get_current_instance_settings()
77
+ if isettings is not None:
78
+ if reload_module and settings.auto_connect:
79
+ if not django.IS_SETUP:
80
+ django.setup_django(isettings)
81
+ if from_module is not None:
82
+ # this only reloads `from_module`
83
+ il.reload(il.import_module(from_module))
84
+ else:
85
+ # this bulk reloads all schema modules
86
+ reload_schema_modules(isettings)
87
+ logger.important(f"connected lamindb: {isettings.slug}")
88
+ return django.IS_SETUP
89
+ else:
90
+ if reload_module and settings.auto_connect:
91
+ logger.warning(InstanceNotSetupError.default_message)
92
+ return False
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from lamin_utils import logger
4
+
5
+ from .core._settings import settings
6
+ from .core._settings_store import current_instance_settings_file
7
+ from .core._setup_bionty_sources import delete_bionty_sources_yaml
8
+ from .core.cloud_sqlite_locker import clear_locker
9
+
10
+
11
+ def close(mute: bool = False) -> None:
12
+ """Close existing instance.
13
+
14
+ Returns `None` if succeeds, otherwise an exception is raised.
15
+ """
16
+ if current_instance_settings_file().exists():
17
+ instance = settings.instance.slug
18
+ try:
19
+ settings.instance._update_cloud_sqlite_file()
20
+ except Exception as e:
21
+ if isinstance(e, FileNotFoundError):
22
+ logger.warning("did not find local cache file")
23
+ elif isinstance(e, PermissionError):
24
+ logger.warning("did not upload cache file - not enough permissions")
25
+ else:
26
+ raise e
27
+ if "bionty" in settings.instance.schema:
28
+ delete_bionty_sources_yaml()
29
+ current_instance_settings_file().unlink()
30
+ clear_locker()
31
+ if not mute:
32
+ logger.success(f"closed instance: {instance}")
33
+ else:
34
+ if not mute:
35
+ logger.info("no instance loaded")
@@ -0,0 +1,429 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from typing import TYPE_CHECKING
6
+ from uuid import UUID
7
+
8
+ from lamin_utils import logger
9
+
10
+ from ._check_setup import _check_instance_setup
11
+ from ._close import close as close_instance
12
+ from ._init_instance import MESSAGE_NO_MULTIPLE_INSTANCE, load_from_isettings
13
+ from ._silence_loggers import silence_loggers
14
+ from .core._hub_core import connect_instance_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._settings_user import UserSettings
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
+ access_token: str | None = None,
119
+ ) -> InstanceSettings:
120
+ settings_file = instance_settings_file(name, owner)
121
+ make_hub_request = True
122
+ if settings_file.exists():
123
+ isettings = load_instance_settings(settings_file)
124
+ # skip hub request for a purely local instance
125
+ make_hub_request = isettings.is_remote
126
+ if make_hub_request:
127
+ # the following will return a string if the instance does not exist
128
+ # on the hub
129
+ # do not call hub if the user is anonymous
130
+ if owner != "anonymous":
131
+ hub_result = connect_instance_hub(
132
+ owner=owner, name=name, access_token=access_token
133
+ )
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
+ api_url=instance_result["api_url"],
164
+ schema_id=None
165
+ if (schema_id := instance_result["schema_id"]) is None
166
+ else UUID(schema_id),
167
+ )
168
+ else:
169
+ if hub_result != "anonymous-user":
170
+ message = INSTANCE_NOT_FOUND_MESSAGE.format(
171
+ owner=owner, name=name, hub_result=hub_result
172
+ )
173
+ else:
174
+ message = "It is not possible to load an anonymous-owned instance from the hub"
175
+ if settings_file.exists():
176
+ isettings = load_instance_settings(settings_file)
177
+ if isettings.is_remote:
178
+ raise InstanceNotFoundError(message)
179
+ else:
180
+ raise InstanceNotFoundError(message)
181
+ return isettings
182
+
183
+
184
+ @unlock_cloud_sqlite_upon_exception(ignore_prev_locker=True)
185
+ def connect(slug: str, **kwargs) -> str | tuple | None:
186
+ """Connect to instance.
187
+
188
+ Args:
189
+ slug: The instance slug `account_handle/instance_name` or URL.
190
+ If the instance is owned by you, it suffices to pass the instance name.
191
+ """
192
+ # validate kwargs
193
+ valid_kwargs = {
194
+ "_db",
195
+ "_write_settings",
196
+ "_raise_not_found_error",
197
+ "_test",
198
+ "_user",
199
+ }
200
+ for kwarg in kwargs:
201
+ if kwarg not in valid_kwargs:
202
+ raise TypeError(f"connect() got unexpected keyword argument '{kwarg}'")
203
+
204
+ isettings: InstanceSettings = None # type: ignore
205
+ # _db is still needed because it is called in init
206
+ _db: str | None = kwargs.get("_db", None)
207
+ _write_settings: bool = kwargs.get("_write_settings", True)
208
+ _raise_not_found_error: bool = kwargs.get("_raise_not_found_error", True)
209
+ _test: bool = kwargs.get("_test", False)
210
+
211
+ access_token: str | None = None
212
+ _user: UserSettings | None = kwargs.get("_user", None)
213
+ if _user is not None:
214
+ access_token = _user.access_token
215
+
216
+ try:
217
+ owner, name = get_owner_name_from_identifier(slug)
218
+
219
+ if _check_instance_setup() and not _test:
220
+ if (
221
+ settings._instance_exists
222
+ and f"{owner}/{name}" == settings.instance.slug
223
+ ):
224
+ logger.info(f"connected lamindb: {settings.instance.slug}")
225
+ return None
226
+ else:
227
+ raise RuntimeError(MESSAGE_NO_MULTIPLE_INSTANCE)
228
+ elif (
229
+ _write_settings
230
+ and settings._instance_exists
231
+ and f"{owner}/{name}" != settings.instance.slug
232
+ ):
233
+ close_instance(mute=True)
234
+
235
+ try:
236
+ isettings = _connect_instance(
237
+ owner, name, db=_db, access_token=access_token
238
+ )
239
+ except InstanceNotFoundError as e:
240
+ if _raise_not_found_error:
241
+ raise e
242
+ else:
243
+ return "instance-not-found"
244
+ if isinstance(isettings, str):
245
+ return isettings
246
+ # at this point we have checked already that isettings is not a string
247
+ # _user is passed to lock cloud sqite for this user in isettings._load_db()
248
+ # has no effect if _user is None or if not cloud sqlite instance
249
+ isettings._locker_user = _user
250
+ isettings._persist(write_to_disk=_write_settings)
251
+ if _test:
252
+ return None
253
+ silence_loggers()
254
+ check, msg = isettings._load_db()
255
+ if not check:
256
+ local_db = (
257
+ isettings._is_cloud_sqlite and isettings._sqlite_file_local.exists()
258
+ )
259
+ if local_db:
260
+ logger.warning(
261
+ "SQLite file does not exist in the cloud, but exists locally:"
262
+ f" {isettings._sqlite_file_local}\nTo push the file to the cloud,"
263
+ " call: lamin disconnect"
264
+ )
265
+ elif _raise_not_found_error:
266
+ raise SystemExit(msg)
267
+ else:
268
+ logger.warning(
269
+ f"instance exists with id {isettings._id.hex}, but database is not"
270
+ " loadable: re-initializing"
271
+ )
272
+ return "instance-corrupted-or-deleted"
273
+ # this is for testing purposes only
274
+ if _TEST_FAILED_LOAD:
275
+ raise RuntimeError("Technical testing error.")
276
+
277
+ # below is for backfilling the instance_uid value
278
+ # we'll enable it once more people migrated to 0.71.0
279
+ # ssettings_record = isettings.storage.record
280
+ # if ssettings_record.instance_uid is None:
281
+ # ssettings_record.instance_uid = isettings.uid
282
+ # # try saving if not read-only access
283
+ # try:
284
+ # ssettings_record.save()
285
+ # # raised by django when the access is denied
286
+ # except ProgrammingError:
287
+ # pass
288
+ load_from_isettings(isettings, user=_user, write_settings=_write_settings)
289
+ except Exception as e:
290
+ if isettings is not None:
291
+ if _write_settings:
292
+ isettings._get_settings_file().unlink(missing_ok=True) # type: ignore
293
+ settings._instance_settings = None
294
+ raise e
295
+ # rename lnschema_bionty to bionty for sql tables
296
+ if "bionty" in isettings.schema:
297
+ no_lnschema_bionty_file = (
298
+ settings_dir / f"no_lnschema_bionty-{isettings.slug.replace('/', '')}"
299
+ )
300
+ if not no_lnschema_bionty_file.exists():
301
+ migrate_lnschema_bionty(
302
+ isettings, no_lnschema_bionty_file, write_file=_write_settings
303
+ )
304
+ return None
305
+
306
+
307
+ def load(slug: str) -> str | tuple | None:
308
+ """Connect to instance and set ``auto-connect`` to true.
309
+
310
+ This is exactly the same as ``ln.connect()`` except for that
311
+ ``ln.connect()`` doesn't change the state of ``auto-connect``.
312
+ """
313
+ print("Warning: This is deprecated and will be removed.")
314
+ result = connect(slug)
315
+ settings.auto_connect = True
316
+ return result
317
+
318
+
319
+ def migrate_lnschema_bionty(
320
+ isettings: InstanceSettings, no_lnschema_bionty_file: Path, write_file: bool = True
321
+ ):
322
+ """Migrate lnschema_bionty tables to bionty tables if bionty_source doesn't exist.
323
+
324
+ :param db_uri: str, database URI (e.g., 'sqlite:///path/to/db.sqlite' or 'postgresql://user:password@host:port/dbname')
325
+ """
326
+ from urllib.parse import urlparse
327
+
328
+ parsed_uri = urlparse(isettings.db)
329
+ db_type = parsed_uri.scheme
330
+
331
+ if db_type == "sqlite":
332
+ import sqlite3
333
+
334
+ conn = sqlite3.connect(parsed_uri.path)
335
+ elif db_type in ["postgresql", "postgres"]:
336
+ import psycopg2
337
+
338
+ conn = psycopg2.connect(isettings.db)
339
+ else:
340
+ raise ValueError("Unsupported database type. Use 'sqlite' or 'postgresql' URI.")
341
+
342
+ cur = conn.cursor()
343
+
344
+ try:
345
+ # check if bionty_source table exists
346
+ if db_type == "sqlite":
347
+ cur.execute(
348
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='bionty_source'"
349
+ )
350
+ migrated = cur.fetchone() is not None
351
+
352
+ # tables that need to be renamed
353
+ cur.execute(
354
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'lnschema_bionty_%'"
355
+ )
356
+ tables_to_rename = [
357
+ row[0][len("lnschema_bionty_") :] for row in cur.fetchall()
358
+ ]
359
+ else: # postgres
360
+ cur.execute(
361
+ "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'bionty_source')"
362
+ )
363
+ migrated = cur.fetchone()[0]
364
+
365
+ # tables that need to be renamed
366
+ cur.execute(
367
+ "SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'lnschema_bionty_%'"
368
+ )
369
+ tables_to_rename = [
370
+ row[0][len("lnschema_bionty_") :] for row in cur.fetchall()
371
+ ]
372
+
373
+ if migrated:
374
+ if write_file:
375
+ no_lnschema_bionty_file.touch(exist_ok=True)
376
+ else:
377
+ try:
378
+ # rename tables only if bionty_source doesn't exist and there are tables to rename
379
+ for table in tables_to_rename:
380
+ if db_type == "sqlite":
381
+ cur.execute(
382
+ f"ALTER TABLE lnschema_bionty_{table} RENAME TO bionty_{table}"
383
+ )
384
+ else: # postgres
385
+ cur.execute(
386
+ f"ALTER TABLE lnschema_bionty_{table} RENAME TO bionty_{table};"
387
+ )
388
+
389
+ # update django_migrations table
390
+ cur.execute(
391
+ "UPDATE django_migrations SET app = 'bionty' WHERE app = 'lnschema_bionty'"
392
+ )
393
+
394
+ logger.warning(
395
+ "Please uninstall lnschema-bionty via `pip uninstall lnschema-bionty`!"
396
+ )
397
+ if write_file:
398
+ no_lnschema_bionty_file.touch(exist_ok=True)
399
+ except Exception:
400
+ # read-only users can't rename tables
401
+ pass
402
+
403
+ conn.commit()
404
+
405
+ except Exception as e:
406
+ print(f"An error occurred: {e}")
407
+ conn.rollback()
408
+
409
+ finally:
410
+ # close the cursor and connection
411
+ cur.close()
412
+ conn.close()
413
+
414
+
415
+ def get_owner_name_from_identifier(identifier: str):
416
+ if "/" in identifier:
417
+ if identifier.startswith("https://lamin.ai/"):
418
+ identifier = identifier.replace("https://lamin.ai/", "")
419
+ split = identifier.split("/")
420
+ if len(split) > 2:
421
+ raise ValueError(
422
+ "The instance identifier needs to be 'owner/name', the instance name"
423
+ " (owner is current user) or the URL: https://lamin.ai/owner/name."
424
+ )
425
+ owner, name = split
426
+ else:
427
+ owner = settings.user.handle
428
+ name = identifier
429
+ return owner, name