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,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`."""
|
lamindb_setup/_cache.py
ADDED
|
@@ -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
|
lamindb_setup/_check.py
ADDED
|
@@ -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
|
lamindb_setup/_close.py
ADDED
|
@@ -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
|