lamindb_setup 1.16.0__py3-none-any.whl → 1.17.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 +6 -3
- lamindb_setup/_check_setup.py +24 -85
- lamindb_setup/_connect_instance.py +9 -23
- lamindb_setup/_delete.py +10 -5
- lamindb_setup/_disconnect.py +12 -9
- lamindb_setup/_init_instance.py +0 -1
- lamindb_setup/_migrate.py +0 -14
- lamindb_setup/_setup_user.py +8 -5
- lamindb_setup/_silence_loggers.py +2 -0
- lamindb_setup/core/_aws_options.py +17 -7
- lamindb_setup/core/_hub_client.py +1 -2
- lamindb_setup/core/_hub_core.py +9 -6
- lamindb_setup/core/_settings.py +14 -10
- lamindb_setup/core/_settings_instance.py +28 -6
- lamindb_setup/core/_settings_load.py +25 -7
- lamindb_setup/core/django.py +46 -18
- lamindb_setup/core/lamin.db.gz +0 -0
- lamindb_setup/core/upath.py +15 -6
- lamindb_setup/errors.py +0 -12
- lamindb_setup/io.py +16 -5
- {lamindb_setup-1.16.0.dist-info → lamindb_setup-1.17.0.dist-info}/METADATA +4 -5
- {lamindb_setup-1.16.0.dist-info → lamindb_setup-1.17.0.dist-info}/RECORD +24 -23
- {lamindb_setup-1.16.0.dist-info → lamindb_setup-1.17.0.dist-info}/WHEEL +1 -1
- {lamindb_setup-1.16.0.dist-info → lamindb_setup-1.17.0.dist-info/licenses}/LICENSE +0 -0
lamindb_setup/__init__.py
CHANGED
|
@@ -35,14 +35,17 @@ Migration management
|
|
|
35
35
|
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
__version__ = "1.
|
|
38
|
+
__version__ = "1.17.0" # denote a release candidate for 0.1.0 with 0.1rc1
|
|
39
39
|
|
|
40
40
|
import os
|
|
41
41
|
import warnings
|
|
42
42
|
|
|
43
|
-
# ignore for now,
|
|
43
|
+
# ignore for now, this is for timeout parameter,
|
|
44
|
+
# it is more convenient to specify it directly for now
|
|
44
45
|
warnings.filterwarnings("ignore", category=DeprecationWarning, module="supabase")
|
|
45
|
-
warnings.filterwarnings(
|
|
46
|
+
warnings.filterwarnings(
|
|
47
|
+
"ignore", category=DeprecationWarning, module="supabase_functions"
|
|
48
|
+
)
|
|
46
49
|
warnings.filterwarnings("ignore", category=DeprecationWarning, module="postgrest")
|
|
47
50
|
|
|
48
51
|
from packaging import version as packaging_version
|
lamindb_setup/_check_setup.py
CHANGED
|
@@ -4,7 +4,6 @@ import functools
|
|
|
4
4
|
import importlib as il
|
|
5
5
|
import inspect
|
|
6
6
|
import os
|
|
7
|
-
from importlib.metadata import distributions
|
|
8
7
|
from typing import TYPE_CHECKING
|
|
9
8
|
from uuid import UUID
|
|
10
9
|
|
|
@@ -16,7 +15,6 @@ from .core._settings import settings
|
|
|
16
15
|
from .core._settings_store import current_instance_settings_file
|
|
17
16
|
from .errors import (
|
|
18
17
|
MODULE_WASNT_CONFIGURED_MESSAGE_TEMPLATE,
|
|
19
|
-
InstanceNotSetupError,
|
|
20
18
|
ModuleWasntConfigured,
|
|
21
19
|
)
|
|
22
20
|
|
|
@@ -26,8 +24,6 @@ if TYPE_CHECKING:
|
|
|
26
24
|
from .core._settings_instance import InstanceSettings
|
|
27
25
|
|
|
28
26
|
|
|
29
|
-
CURRENT_ISETTINGS: InstanceSettings | None = None
|
|
30
|
-
MODULE_CANDIDATES: set[str] | None = None
|
|
31
27
|
IS_LOADING: bool = False
|
|
32
28
|
|
|
33
29
|
|
|
@@ -45,55 +41,6 @@ def disable_auto_connect(func: Callable):
|
|
|
45
41
|
return wrapper
|
|
46
42
|
|
|
47
43
|
|
|
48
|
-
def find_module_candidates():
|
|
49
|
-
"""Find all local packages that depend on lamindb."""
|
|
50
|
-
global MODULE_CANDIDATES
|
|
51
|
-
if MODULE_CANDIDATES is not None:
|
|
52
|
-
return MODULE_CANDIDATES
|
|
53
|
-
all_dists = list(distributions())
|
|
54
|
-
lamindb_deps = {
|
|
55
|
-
dist.metadata["Name"].lower()
|
|
56
|
-
for dist in all_dists
|
|
57
|
-
if dist.requires and any("lamindb" in req.lower() for req in dist.requires)
|
|
58
|
-
}
|
|
59
|
-
lamindb_deps.remove("lamindb")
|
|
60
|
-
MODULE_CANDIDATES = lamindb_deps
|
|
61
|
-
return lamindb_deps
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _get_current_instance_settings(from_module: str | None = None) -> InstanceSettings:
|
|
65
|
-
from .core._settings_instance import InstanceSettings
|
|
66
|
-
|
|
67
|
-
global CURRENT_ISETTINGS
|
|
68
|
-
|
|
69
|
-
if CURRENT_ISETTINGS is not None:
|
|
70
|
-
return CURRENT_ISETTINGS
|
|
71
|
-
if current_instance_settings_file().exists():
|
|
72
|
-
from .core._settings_load import load_instance_settings
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
isettings = load_instance_settings()
|
|
76
|
-
except Exception as e:
|
|
77
|
-
# user will get more detailed traceback once they run the CLI
|
|
78
|
-
logger.error(
|
|
79
|
-
"Current instance cannot be reached, disconnect from it: `lamin disconnect`\n"
|
|
80
|
-
"Alternatively, init or load a connectable instance on the"
|
|
81
|
-
" command line: `lamin connect <instance>` or `lamin init <...>`"
|
|
82
|
-
)
|
|
83
|
-
raise e
|
|
84
|
-
else:
|
|
85
|
-
module_candidates = find_module_candidates()
|
|
86
|
-
isettings = InstanceSettings(
|
|
87
|
-
id=UUID("00000000-0000-0000-0000-000000000000"),
|
|
88
|
-
owner="none",
|
|
89
|
-
name="none",
|
|
90
|
-
storage=None,
|
|
91
|
-
modules=",".join(module_candidates),
|
|
92
|
-
)
|
|
93
|
-
CURRENT_ISETTINGS = isettings
|
|
94
|
-
return isettings
|
|
95
|
-
|
|
96
|
-
|
|
97
44
|
def _normalize_module_name(module_name: str) -> str:
|
|
98
45
|
return module_name.replace("lnschema_", "").replace("_", "-")
|
|
99
46
|
|
|
@@ -137,12 +84,9 @@ def _infer_callers_module_name() -> str | None:
|
|
|
137
84
|
# users should not see it
|
|
138
85
|
def _check_instance_setup(from_module: str | None = None) -> bool:
|
|
139
86
|
if django_lamin.IS_SETUP:
|
|
140
|
-
# reload logic here because module might not yet have been imported
|
|
141
|
-
# upon first setup
|
|
142
87
|
if from_module is not None:
|
|
143
88
|
if from_module != "lamindb":
|
|
144
89
|
_check_module_in_instance_modules(from_module)
|
|
145
|
-
il.reload(il.import_module(from_module))
|
|
146
90
|
else:
|
|
147
91
|
infer_module = _infer_callers_module_name()
|
|
148
92
|
if infer_module is not None and infer_module not in {
|
|
@@ -159,34 +103,29 @@ def _check_instance_setup(from_module: str | None = None) -> bool:
|
|
|
159
103
|
"errors in regular lamindb usage"
|
|
160
104
|
)
|
|
161
105
|
return True
|
|
162
|
-
|
|
163
|
-
if
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if isettings.slug != "none/none":
|
|
174
|
-
logger.important(f"connected lamindb: {isettings.slug}")
|
|
175
|
-
# update of local storage location through search_local_root()
|
|
176
|
-
settings._instance_settings = isettings
|
|
177
|
-
else:
|
|
178
|
-
logger.warning("not connected, call: ln.connect('account/name')")
|
|
106
|
+
|
|
107
|
+
if IS_LOADING or from_module is None:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
not settings._instance_exists
|
|
112
|
+
and os.environ.get("LAMIN_CURRENT_INSTANCE") is not None
|
|
113
|
+
):
|
|
114
|
+
from ._connect_instance import connect
|
|
115
|
+
|
|
116
|
+
connect(_write_settings=False, _reload_lamindb=False)
|
|
179
117
|
return django_lamin.IS_SETUP
|
|
180
118
|
else:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
119
|
+
isettings = settings.instance
|
|
120
|
+
if from_module != "lamindb":
|
|
121
|
+
_check_module_in_instance_modules(from_module, isettings)
|
|
122
|
+
|
|
123
|
+
import lamindb # connect to the instance
|
|
124
|
+
else:
|
|
125
|
+
# disable_auto_connect to avoid triggering _check_instance_setup in modules
|
|
126
|
+
disable_auto_connect(django_lamin.setup_django)(isettings)
|
|
127
|
+
if isettings.slug != "none/none":
|
|
128
|
+
logger.important(f"connected lamindb: {isettings.slug}")
|
|
129
|
+
# update of local storage location through search_local_root()
|
|
130
|
+
settings._instance_settings = isettings
|
|
131
|
+
return django_lamin.IS_SETUP
|
|
@@ -9,10 +9,7 @@ from uuid import UUID
|
|
|
9
9
|
|
|
10
10
|
from lamin_utils import logger
|
|
11
11
|
|
|
12
|
-
from ._check_setup import
|
|
13
|
-
_check_instance_setup,
|
|
14
|
-
_get_current_instance_settings,
|
|
15
|
-
)
|
|
12
|
+
from ._check_setup import _check_instance_setup
|
|
16
13
|
from ._disconnect import disconnect
|
|
17
14
|
from ._init_instance import load_from_isettings
|
|
18
15
|
from ._silence_loggers import silence_loggers
|
|
@@ -266,13 +263,8 @@ def validate_connection_state(
|
|
|
266
263
|
from django.db import connection
|
|
267
264
|
|
|
268
265
|
if (
|
|
269
|
-
settings._instance_exists
|
|
266
|
+
settings._instance_exists # exists only for real instances, not for none/none
|
|
270
267
|
and f"{owner}/{name}" == settings.instance.slug
|
|
271
|
-
# below is to ensure that if another process interferes
|
|
272
|
-
# we don't use the in-memory mock database
|
|
273
|
-
# could be made more specific by checking whether the django
|
|
274
|
-
# configured database is the same as the one in settings
|
|
275
|
-
and connection.settings_dict["NAME"] != ":memory:"
|
|
276
268
|
and not use_root_db_user # always re-connect for root db user
|
|
277
269
|
):
|
|
278
270
|
logger.important(
|
|
@@ -280,7 +272,7 @@ def validate_connection_state(
|
|
|
280
272
|
)
|
|
281
273
|
return None
|
|
282
274
|
else:
|
|
283
|
-
if settings._instance_exists
|
|
275
|
+
if settings._instance_exists:
|
|
284
276
|
import lamindb as ln
|
|
285
277
|
|
|
286
278
|
if ln.context.transform is not None:
|
|
@@ -292,7 +284,9 @@ def validate_connection_state(
|
|
|
292
284
|
|
|
293
285
|
@unlock_cloud_sqlite_upon_exception(ignore_prev_locker=True)
|
|
294
286
|
def connect(instance: str | None = None, **kwargs: Any) -> str | tuple | None:
|
|
295
|
-
"""Connect
|
|
287
|
+
"""Connect the global default instance.
|
|
288
|
+
|
|
289
|
+
If you want to create a read-only database client, use :class:`~lamindb.DB` instead.
|
|
296
290
|
|
|
297
291
|
Args:
|
|
298
292
|
instance: Pass a slug (`account/name`) or URL (`https://lamin.ai/account/name`).
|
|
@@ -340,12 +334,9 @@ def connect(instance: str | None = None, **kwargs: Any) -> str | tuple | None:
|
|
|
340
334
|
if settings._instance_exists:
|
|
341
335
|
isettings = settings.instance
|
|
342
336
|
else:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
"No instance was connected through the CLI, pass a value to `instance` or connect via the CLI."
|
|
347
|
-
)
|
|
348
|
-
isettings = isettings_or_none
|
|
337
|
+
raise ValueError(
|
|
338
|
+
"No instance was connected through the CLI, pass a value to `instance` or connect via the CLI."
|
|
339
|
+
)
|
|
349
340
|
if use_root_db_user:
|
|
350
341
|
reset_django()
|
|
351
342
|
owner, name = isettings.owner, isettings.name
|
|
@@ -414,7 +405,6 @@ def connect(instance: str | None = None, **kwargs: Any) -> str | tuple | None:
|
|
|
414
405
|
|
|
415
406
|
load_from_isettings(isettings, user=_user, write_settings=_write_settings)
|
|
416
407
|
if _reload_lamindb:
|
|
417
|
-
importlib.reload(importlib.import_module("lamindb"))
|
|
418
408
|
reset_django_module_variables()
|
|
419
409
|
if isettings.slug != "none/none":
|
|
420
410
|
logger.important(f"connected lamindb: {isettings.slug}")
|
|
@@ -424,10 +414,6 @@ def connect(instance: str | None = None, **kwargs: Any) -> str | tuple | None:
|
|
|
424
414
|
isettings._get_settings_file().unlink(missing_ok=True) # type: ignore
|
|
425
415
|
settings._instance_settings = None
|
|
426
416
|
raise e
|
|
427
|
-
if settings.dev_dir is None:
|
|
428
|
-
logger.important_hint(
|
|
429
|
-
"to map a local dev directory, set: ln.setup.settings.dev_dir = '.'"
|
|
430
|
-
)
|
|
431
417
|
return None
|
|
432
418
|
|
|
433
419
|
|
lamindb_setup/_delete.py
CHANGED
|
@@ -11,6 +11,7 @@ from .core._aws_options import HOSTED_BUCKETS
|
|
|
11
11
|
from .core._hub_core import delete_instance as delete_instance_on_hub
|
|
12
12
|
from .core._hub_core import get_storage_records_for_instance
|
|
13
13
|
from .core._settings import settings
|
|
14
|
+
from .core._settings_load import load_instance_settings
|
|
14
15
|
from .core._settings_storage import StorageSettings
|
|
15
16
|
from .core.upath import LocalPathClasses, check_storage_is_empty
|
|
16
17
|
|
|
@@ -38,6 +39,8 @@ def delete_exclusion_dir(isettings: InstanceSettings) -> None:
|
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def delete_by_isettings(isettings: InstanceSettings) -> None:
|
|
42
|
+
assert isettings.slug != "none/none"
|
|
43
|
+
|
|
41
44
|
settings_file = isettings._get_settings_file()
|
|
42
45
|
if settings_file.exists():
|
|
43
46
|
settings_file.unlink()
|
|
@@ -51,12 +54,14 @@ def delete_by_isettings(isettings: InstanceSettings) -> None:
|
|
|
51
54
|
"Did not have permission to delete SQLite file:"
|
|
52
55
|
f" {isettings._sqlite_file}"
|
|
53
56
|
)
|
|
54
|
-
pass
|
|
55
57
|
# unset the global instance settings
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
settings.
|
|
58
|
+
isettings_on_disk = load_instance_settings()
|
|
59
|
+
if isettings_on_disk.slug == isettings.slug:
|
|
60
|
+
settings._instance_settings_path.unlink() # current instance settings file
|
|
61
|
+
# settings.instance can differ from instance in current_settings_file()
|
|
62
|
+
# due to connect() in the same process
|
|
63
|
+
if settings.instance.slug == isettings.slug:
|
|
64
|
+
settings._instance_settings = None
|
|
60
65
|
|
|
61
66
|
|
|
62
67
|
def delete(slug: str, force: bool = False, require_empty: bool = True) -> int | None:
|
lamindb_setup/_disconnect.py
CHANGED
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from lamin_utils import logger
|
|
4
4
|
|
|
5
5
|
from .core._settings import settings
|
|
6
|
-
from .core.
|
|
6
|
+
from .core._settings_load import load_instance_settings
|
|
7
7
|
from .core.cloud_sqlite_locker import clear_locker
|
|
8
8
|
|
|
9
9
|
|
|
@@ -15,10 +15,11 @@ def disconnect(mute: bool = False) -> None:
|
|
|
15
15
|
See Also:
|
|
16
16
|
Clear default instance configuration via the CLI, see `here <https://docs.lamin.ai/cli#disconnect>`__.
|
|
17
17
|
"""
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
# settings._instance_exists can be true due to connect even without having a file
|
|
19
|
+
if settings._instance_exists:
|
|
20
|
+
instance = settings.instance
|
|
20
21
|
try:
|
|
21
|
-
|
|
22
|
+
instance._update_cloud_sqlite_file()
|
|
22
23
|
except Exception as e:
|
|
23
24
|
if isinstance(e, FileNotFoundError):
|
|
24
25
|
logger.warning("did not find local cache file")
|
|
@@ -26,10 +27,12 @@ def disconnect(mute: bool = False) -> None:
|
|
|
26
27
|
logger.warning("did not upload cache file - not enough permissions")
|
|
27
28
|
else:
|
|
28
29
|
raise e
|
|
29
|
-
current_instance_settings_file().unlink()
|
|
30
30
|
clear_locker()
|
|
31
|
+
# instance in current instance file can differ from instance in settings
|
|
32
|
+
if load_instance_settings().slug == instance.slug:
|
|
33
|
+
settings._instance_settings_path.unlink(missing_ok=True)
|
|
34
|
+
settings._instance_settings = None
|
|
31
35
|
if not mute:
|
|
32
|
-
logger.success(f"disconnected instance: {instance}")
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
logger.info("no instance loaded")
|
|
36
|
+
logger.success(f"disconnected instance: {instance.slug}")
|
|
37
|
+
elif not mute:
|
|
38
|
+
logger.info("no instance loaded")
|
lamindb_setup/_init_instance.py
CHANGED
|
@@ -340,7 +340,6 @@ def init(
|
|
|
340
340
|
from ._schema_metadata import update_schema_in_hub
|
|
341
341
|
|
|
342
342
|
update_schema_in_hub(access_token=access_token)
|
|
343
|
-
importlib.reload(importlib.import_module("lamindb"))
|
|
344
343
|
reset_django_module_variables()
|
|
345
344
|
logger.important(f"initialized lamindb: {isettings.slug}")
|
|
346
345
|
except Exception as e:
|
lamindb_setup/_migrate.py
CHANGED
|
@@ -125,24 +125,10 @@ class migrate:
|
|
|
125
125
|
from lamindb_setup._schema_metadata import update_schema_in_hub
|
|
126
126
|
from lamindb_setup.core._hub_client import call_with_fallback_auth
|
|
127
127
|
from lamindb_setup.core._hub_crud import (
|
|
128
|
-
select_collaborator,
|
|
129
128
|
update_instance,
|
|
130
129
|
)
|
|
131
130
|
|
|
132
131
|
if settings.instance.is_on_hub:
|
|
133
|
-
# double check that user is an admin, otherwise will fail below
|
|
134
|
-
# due to insufficient SQL permissions with cryptic error
|
|
135
|
-
collaborator = call_with_fallback_auth(
|
|
136
|
-
select_collaborator,
|
|
137
|
-
instance_id=settings.instance._id,
|
|
138
|
-
account_id=settings.user._uuid,
|
|
139
|
-
fine_grained_access=settings.instance._fine_grained_access,
|
|
140
|
-
)
|
|
141
|
-
if collaborator is None or collaborator["role"] != "admin":
|
|
142
|
-
raise SystemExit(
|
|
143
|
-
"❌ Only admins can deploy migrations, please ensure that you're an"
|
|
144
|
-
f" admin: https://lamin.ai/{settings.instance.slug}/settings"
|
|
145
|
-
)
|
|
146
132
|
# ensure we connect with the root user
|
|
147
133
|
if "root" not in settings.instance.db:
|
|
148
134
|
connect(use_root_db_user=True)
|
lamindb_setup/_setup_user.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from time import sleep
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from lamin_utils import logger
|
|
@@ -59,15 +60,14 @@ def login(
|
|
|
59
60
|
|
|
60
61
|
`login()` prompts for your API key unless you set it via the `LAMIN_API_KEY` environment variable or pass it as an argument.
|
|
61
62
|
|
|
62
|
-
You can create your API key in your account settings on LaminHub
|
|
63
|
+
You can create your API key in your account settings on LaminHub at `lamin.ai/settings <https://lamin.ai/settings>`__.
|
|
64
|
+
|
|
65
|
+
Note that the preferred method is to use the CLI command `lamin login`: `docs.lamin.ai/cli#login <https://docs.lamin.ai/cli#login>`__.
|
|
63
66
|
|
|
64
67
|
Args:
|
|
65
68
|
user: User handle.
|
|
66
69
|
api_key: API key.
|
|
67
70
|
|
|
68
|
-
See Also:
|
|
69
|
-
Login via the CLI command `lamin login`, see `here <https://docs.lamin.ai/cli#login>`__.
|
|
70
|
-
|
|
71
71
|
Examples:
|
|
72
72
|
|
|
73
73
|
Logging in the first time::
|
|
@@ -80,12 +80,15 @@ def login(
|
|
|
80
80
|
|
|
81
81
|
ln.setup.login("myhandle") # pass your user handle
|
|
82
82
|
"""
|
|
83
|
+
from getpass import getpass
|
|
84
|
+
|
|
83
85
|
if user is None:
|
|
84
86
|
if api_key is None:
|
|
85
87
|
if "LAMIN_API_KEY" in os.environ:
|
|
86
88
|
api_key = os.environ["LAMIN_API_KEY"]
|
|
87
89
|
else:
|
|
88
|
-
|
|
90
|
+
print("Copy your API key. To create one: https://lamin.ai/settings")
|
|
91
|
+
api_key = getpass("Paste it: ")
|
|
89
92
|
elif api_key is not None:
|
|
90
93
|
raise ValueError("Please provide either 'user' or 'api_key', not both.")
|
|
91
94
|
|
|
@@ -25,6 +25,8 @@ def silence_loggers():
|
|
|
25
25
|
set_stream_logger(name="botocore.auth", level=logging.WARNING)
|
|
26
26
|
set_stream_logger(name="botocore.endpoint", level=logging.WARNING)
|
|
27
27
|
set_stream_logger(name="httpx", level=logging.WARNING)
|
|
28
|
+
set_stream_logger(name="httpcore", level=logging.WARNING)
|
|
29
|
+
set_stream_logger(name="hpack", level=logging.WARNING)
|
|
28
30
|
try:
|
|
29
31
|
import aiobotocore
|
|
30
32
|
|
|
@@ -60,6 +60,7 @@ class AWSOptionsManager:
|
|
|
60
60
|
from aiobotocore.session import AioSession
|
|
61
61
|
from s3fs import S3FileSystem
|
|
62
62
|
|
|
63
|
+
anon_env = os.getenv("LAMIN_S3_ANON") == "true"
|
|
63
64
|
# this is cached so will be resued with the connection initialized
|
|
64
65
|
# these options are set for paths in _path_inject_options
|
|
65
66
|
# here we set the same options to cache the filesystem
|
|
@@ -68,19 +69,28 @@ class AWSOptionsManager:
|
|
|
68
69
|
use_listings_cache=True,
|
|
69
70
|
version_aware=False,
|
|
70
71
|
config_kwargs={"max_pool_connections": 64},
|
|
72
|
+
anon=anon_env,
|
|
71
73
|
)
|
|
72
74
|
|
|
73
75
|
self._suppress_aiobotocore_traceback_logging()
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
self.anon: bool = fs.session._credentials is None
|
|
78
|
-
except Exception as e:
|
|
77
|
+
if anon_env:
|
|
78
|
+
self.anon: bool = True
|
|
79
79
|
logger.warning(
|
|
80
|
-
|
|
81
|
-
"
|
|
80
|
+
"`anon` mode will be used for all non-managed buckets "
|
|
81
|
+
"because the environment variable LAMIN_S3_ANON was set to 'true'"
|
|
82
82
|
)
|
|
83
|
-
|
|
83
|
+
else:
|
|
84
|
+
try:
|
|
85
|
+
fs.connect()
|
|
86
|
+
self.anon = fs.session._credentials is None
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.warning(
|
|
89
|
+
f"There is a problem with your default AWS Credentials: {e}\n"
|
|
90
|
+
"`anon` mode will be used for all non-managed buckets"
|
|
91
|
+
)
|
|
92
|
+
self.anon = True
|
|
93
|
+
|
|
84
94
|
self.anon_public: bool | None = None
|
|
85
95
|
if not self.anon:
|
|
86
96
|
try:
|
|
@@ -11,8 +11,7 @@ import httpx
|
|
|
11
11
|
from httpx_retries import Retry, RetryTransport
|
|
12
12
|
from lamin_utils import logger
|
|
13
13
|
from pydantic_settings import BaseSettings
|
|
14
|
-
from supabase import Client, create_client
|
|
15
|
-
from supabase.lib.client_options import ClientOptions
|
|
14
|
+
from supabase import Client, ClientOptions, create_client
|
|
16
15
|
|
|
17
16
|
from ._settings_save import save_user_settings
|
|
18
17
|
|
lamindb_setup/core/_hub_core.py
CHANGED
|
@@ -383,7 +383,8 @@ def _init_instance_hub(
|
|
|
383
383
|
) -> None:
|
|
384
384
|
from ._settings import settings
|
|
385
385
|
|
|
386
|
-
|
|
386
|
+
created_by_id = settings.user._uuid.hex if account_id is None else account_id.hex # type: ignore
|
|
387
|
+
owner_account_id = os.getenv("LAMINDB_ACCOUNT_ID_INIT", created_by_id)
|
|
387
388
|
|
|
388
389
|
try:
|
|
389
390
|
lamindb_version = metadata.version("lamindb")
|
|
@@ -391,13 +392,13 @@ def _init_instance_hub(
|
|
|
391
392
|
lamindb_version = None
|
|
392
393
|
fields = {
|
|
393
394
|
"id": isettings._id.hex,
|
|
394
|
-
"account_id":
|
|
395
|
+
"account_id": owner_account_id,
|
|
395
396
|
"name": isettings.name,
|
|
396
397
|
"lnid": isettings.uid,
|
|
397
398
|
"schema_str": isettings._schema_str,
|
|
398
399
|
"lamindb_version": lamindb_version,
|
|
399
400
|
"public": False,
|
|
400
|
-
"created_by_id":
|
|
401
|
+
"created_by_id": created_by_id,
|
|
401
402
|
}
|
|
402
403
|
if isettings.dialect != "sqlite":
|
|
403
404
|
db_dsn = LaminDsnModel(db=isettings.db)
|
|
@@ -407,7 +408,7 @@ def _init_instance_hub(
|
|
|
407
408
|
"db_port": db_dsn.db.port,
|
|
408
409
|
"db_database": db_dsn.db.database,
|
|
409
410
|
}
|
|
410
|
-
fields.update(db_fields)
|
|
411
|
+
fields.update(db_fields) # type: ignore
|
|
411
412
|
slug = isettings.slug
|
|
412
413
|
# I'd like the following to be an upsert, but this seems to violate RLS
|
|
413
414
|
# Similarly, if we don't specify `returning="minimal"`, we'll violate RLS
|
|
@@ -415,7 +416,9 @@ def _init_instance_hub(
|
|
|
415
416
|
# as then init_instance is no longer idempotent
|
|
416
417
|
try:
|
|
417
418
|
client.table("instance").insert(fields, returning="minimal").execute()
|
|
418
|
-
except APIError:
|
|
419
|
+
except APIError as e:
|
|
420
|
+
if "new row violates row-level security policy" in str(e):
|
|
421
|
+
raise e
|
|
419
422
|
logger.warning(f"instance already existed at: https://lamin.ai/{slug}")
|
|
420
423
|
return None
|
|
421
424
|
if isettings.dialect != "sqlite" and isettings.is_remote:
|
|
@@ -713,7 +716,7 @@ def get_lamin_site_base_url():
|
|
|
713
716
|
|
|
714
717
|
|
|
715
718
|
def sign_up_local_hub(email) -> str | tuple[str, str, str]:
|
|
716
|
-
# raises
|
|
719
|
+
# raises AuthApiError: User already registered
|
|
717
720
|
password = base62(40) # generate new password
|
|
718
721
|
sign_up_kwargs = {"email": email, "password": password}
|
|
719
722
|
client = connect_hub()
|
lamindb_setup/core/_settings.py
CHANGED
|
@@ -46,6 +46,12 @@ def _process_cache_path(cache_path: UPathStr | None) -> UPath | None:
|
|
|
46
46
|
return cache_dir
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
# returned by settings.branch for none/none instance
|
|
50
|
+
class MainBranchMock:
|
|
51
|
+
id = 1
|
|
52
|
+
name = "main"
|
|
53
|
+
|
|
54
|
+
|
|
49
55
|
class SetupSettings:
|
|
50
56
|
"""Setup settings."""
|
|
51
57
|
|
|
@@ -140,6 +146,10 @@ class SetupSettings:
|
|
|
140
146
|
# and we never need a DB request
|
|
141
147
|
def branch(self) -> Branch:
|
|
142
148
|
"""Default branch."""
|
|
149
|
+
# this is needed for .filter() with non-default connections
|
|
150
|
+
if not self._instance_exists:
|
|
151
|
+
return MainBranchMock()
|
|
152
|
+
|
|
143
153
|
if self._branch is None:
|
|
144
154
|
from lamindb import Branch
|
|
145
155
|
|
|
@@ -222,10 +232,9 @@ class SetupSettings:
|
|
|
222
232
|
If `True`, the current instance is connected, meaning that the db and other settings
|
|
223
233
|
are properly configured for use.
|
|
224
234
|
"""
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return False
|
|
235
|
+
from . import django
|
|
236
|
+
|
|
237
|
+
return self._instance_exists and django.IS_SETUP
|
|
229
238
|
|
|
230
239
|
@property
|
|
231
240
|
def private_django_api(self) -> bool:
|
|
@@ -284,12 +293,7 @@ class SetupSettings:
|
|
|
284
293
|
|
|
285
294
|
@property
|
|
286
295
|
def _instance_exists(self):
|
|
287
|
-
|
|
288
|
-
self.instance # noqa
|
|
289
|
-
return True
|
|
290
|
-
# this is implicit logic that catches if no instance is loaded
|
|
291
|
-
except CurrentInstanceNotConfigured:
|
|
292
|
-
return False
|
|
296
|
+
return self.instance.slug != "none/none"
|
|
293
297
|
|
|
294
298
|
@property
|
|
295
299
|
def cache_dir(self) -> UPath:
|
|
@@ -125,10 +125,11 @@ class InstanceSettings:
|
|
|
125
125
|
if self._local_storage is not None:
|
|
126
126
|
value_local = self.local_storage
|
|
127
127
|
representation += f"\n - local storage: {value_local.root_as_str} ({value_local.region})"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
if value is not None:
|
|
129
|
+
representation += (
|
|
130
|
+
f"\n - cloud storage: {value.root_as_str} ({value.region})"
|
|
131
|
+
)
|
|
132
|
+
elif value is not None:
|
|
132
133
|
representation += (
|
|
133
134
|
f"\n - storage: {value.root_as_str} ({value.region})"
|
|
134
135
|
)
|
|
@@ -513,17 +514,36 @@ class InstanceSettings:
|
|
|
513
514
|
|
|
514
515
|
@property
|
|
515
516
|
def dialect(self) -> Literal["sqlite", "postgresql"]:
|
|
516
|
-
"""SQL dialect.
|
|
517
|
+
"""SQL dialect.
|
|
518
|
+
|
|
519
|
+
Equivalent to :attr:`vendor`.
|
|
520
|
+
|
|
521
|
+
"vendor" is the Django terminology for the type of database. "dialect" is the SQLAlchemy terminology.
|
|
522
|
+
"""
|
|
517
523
|
if self._db is None or self._db.startswith("sqlite://"):
|
|
518
524
|
return "sqlite"
|
|
519
525
|
else:
|
|
520
526
|
assert self._db.startswith("postgresql"), f"Unexpected DB value: {self._db}"
|
|
521
527
|
return "postgresql"
|
|
522
528
|
|
|
529
|
+
@property
|
|
530
|
+
def vendor(self) -> Literal["sqlite", "postgresql"]:
|
|
531
|
+
"""Database vendor.
|
|
532
|
+
|
|
533
|
+
Equivalent to :attr:`dialect`.
|
|
534
|
+
|
|
535
|
+
"vendor" is the Django terminology for the type of database. "dialect" is the SQLAlchemy terminology.
|
|
536
|
+
"""
|
|
537
|
+
return self.dialect
|
|
538
|
+
|
|
523
539
|
@property
|
|
524
540
|
def _is_cloud_sqlite(self) -> bool:
|
|
525
541
|
"""Is this a cloud instance with sqlite db."""
|
|
526
|
-
return
|
|
542
|
+
return (
|
|
543
|
+
self.dialect == "sqlite"
|
|
544
|
+
and self.storage is not None
|
|
545
|
+
and self.storage.type_is_cloud
|
|
546
|
+
)
|
|
527
547
|
|
|
528
548
|
@property
|
|
529
549
|
def _cloud_sqlite_locker(self):
|
|
@@ -543,6 +563,8 @@ class InstanceSettings:
|
|
|
543
563
|
@property
|
|
544
564
|
def is_remote(self) -> bool:
|
|
545
565
|
"""Boolean indicating if an instance has no local component."""
|
|
566
|
+
if self.storage is None and self.db == "sqlite:///:memory:":
|
|
567
|
+
return False
|
|
546
568
|
return check_is_instance_remote(self.storage.root_as_str, self.db)
|
|
547
569
|
|
|
548
570
|
@property
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
from importlib.util import find_spec
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import TYPE_CHECKING
|
|
6
7
|
from uuid import UUID, uuid4
|
|
@@ -46,19 +47,36 @@ def load_cache_path_from_settings(storage_settings: Path | None = None) -> Path
|
|
|
46
47
|
return None
|
|
47
48
|
|
|
48
49
|
|
|
50
|
+
def find_module_candidates():
|
|
51
|
+
"""Find all local packages that depend on lamindb."""
|
|
52
|
+
candidates = ["bionty", "wetlab"]
|
|
53
|
+
return [c for c in candidates if find_spec(c) is not None]
|
|
54
|
+
|
|
55
|
+
|
|
49
56
|
def load_instance_settings(instance_settings_file: Path | None = None):
|
|
50
57
|
if instance_settings_file is None:
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
isettings_file = current_instance_settings_file()
|
|
59
|
+
if not isettings_file.exists():
|
|
60
|
+
isettings = InstanceSettings(
|
|
61
|
+
id=UUID("00000000-0000-0000-0000-000000000000"),
|
|
62
|
+
owner="none",
|
|
63
|
+
name="none",
|
|
64
|
+
storage=None,
|
|
65
|
+
modules=",".join(find_module_candidates()),
|
|
66
|
+
)
|
|
67
|
+
return isettings
|
|
68
|
+
else:
|
|
69
|
+
isettings_file = instance_settings_file
|
|
70
|
+
|
|
71
|
+
if not isettings_file.exists():
|
|
72
|
+
# this errors only if the file was explicitly provided
|
|
53
73
|
raise CurrentInstanceNotConfigured
|
|
54
74
|
try:
|
|
55
|
-
settings_store = InstanceSettingsStore(_env_file=
|
|
75
|
+
settings_store = InstanceSettingsStore(_env_file=isettings_file)
|
|
56
76
|
except (ValidationError, TypeError) as error:
|
|
57
|
-
with open(instance_settings_file) as f:
|
|
58
|
-
content = f.read()
|
|
59
77
|
raise SettingsEnvFileOutdated(
|
|
60
|
-
f"\n\n{error}\n\nYour instance settings file with\n\n{
|
|
61
|
-
f" (likely outdated), see validation error. Please delete {
|
|
78
|
+
f"\n\n{error}\n\nYour instance settings file with\n\n{isettings_file.read_text()}\nis invalid"
|
|
79
|
+
f" (likely outdated), see validation error. Please delete {isettings_file} &"
|
|
62
80
|
" reload (remote) or re-initialize (local) the instance with the same name & storage location."
|
|
63
81
|
) from error
|
|
64
82
|
isettings = setup_instance_from_store(settings_store)
|
lamindb_setup/core/django.py
CHANGED
|
@@ -5,13 +5,15 @@ import builtins
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
7
|
import importlib as il
|
|
8
|
+
import gzip
|
|
8
9
|
import jwt
|
|
9
10
|
import time
|
|
10
11
|
import threading
|
|
11
12
|
from pathlib import Path
|
|
13
|
+
import shutil
|
|
12
14
|
from packaging import version
|
|
13
15
|
from ._settings_instance import InstanceSettings, is_local_db_url
|
|
14
|
-
|
|
16
|
+
from ..errors import CurrentInstanceNotConfigured
|
|
15
17
|
from lamin_utils import logger
|
|
16
18
|
|
|
17
19
|
|
|
@@ -21,6 +23,24 @@ IS_MIGRATING = False
|
|
|
21
23
|
CONN_MAX_AGE = 299
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
def get_connection(connection_name: str):
|
|
27
|
+
from django.db import connections
|
|
28
|
+
|
|
29
|
+
return connections[connection_name]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def error_no_instance_wrapper(execute, sql, params, many, context):
|
|
33
|
+
connection = context["connection"]
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
connection.vendor == "sqlite"
|
|
37
|
+
and connection.settings_dict.get("NAME") == ":memory:"
|
|
38
|
+
):
|
|
39
|
+
raise CurrentInstanceNotConfigured
|
|
40
|
+
|
|
41
|
+
return execute(sql, params, many, context)
|
|
42
|
+
|
|
43
|
+
|
|
24
44
|
# db token that refreshes on access if needed
|
|
25
45
|
class DBToken:
|
|
26
46
|
def __init__(
|
|
@@ -64,11 +84,6 @@ class DBTokenManager:
|
|
|
64
84
|
|
|
65
85
|
self.tokens: dict[str, DBToken] = {}
|
|
66
86
|
|
|
67
|
-
def get_connection(self, connection_name: str):
|
|
68
|
-
from django.db import connections
|
|
69
|
-
|
|
70
|
-
return connections[connection_name]
|
|
71
|
-
|
|
72
87
|
def set(self, token: DBToken, connection_name: str = "default"):
|
|
73
88
|
if connection_name in self.tokens:
|
|
74
89
|
return
|
|
@@ -77,11 +92,7 @@ class DBTokenManager:
|
|
|
77
92
|
from django.db.backends.signals import connection_created
|
|
78
93
|
|
|
79
94
|
def set_token_wrapper(execute, sql, params, many, context):
|
|
80
|
-
not_in_atomic_block =
|
|
81
|
-
context is None
|
|
82
|
-
or "connection" not in context
|
|
83
|
-
or not context["connection"].in_atomic_block
|
|
84
|
-
)
|
|
95
|
+
not_in_atomic_block = not context["connection"].in_atomic_block
|
|
85
96
|
# ignore atomic blocks
|
|
86
97
|
if not_in_atomic_block:
|
|
87
98
|
sql = token.token_query + sql
|
|
@@ -98,7 +109,7 @@ class DBTokenManager:
|
|
|
98
109
|
result.nextset()
|
|
99
110
|
return result
|
|
100
111
|
|
|
101
|
-
|
|
112
|
+
get_connection(connection_name).execute_wrappers.append(set_token_wrapper)
|
|
102
113
|
|
|
103
114
|
def connection_callback(sender, connection, **kwargs):
|
|
104
115
|
if (
|
|
@@ -124,7 +135,7 @@ class DBTokenManager:
|
|
|
124
135
|
if connection_name in self.tokens:
|
|
125
136
|
# here we don't use the connection from the closure
|
|
126
137
|
# because Atomic is a single class to manage transactions for all connections
|
|
127
|
-
connection =
|
|
138
|
+
connection = get_connection(connection_name)
|
|
128
139
|
if len(connection.atomic_blocks) == 1:
|
|
129
140
|
token = self.tokens[connection_name]
|
|
130
141
|
# use raw psycopg2 connection here
|
|
@@ -142,7 +153,7 @@ class DBTokenManager:
|
|
|
142
153
|
|
|
143
154
|
from django.db.backends.signals import connection_created
|
|
144
155
|
|
|
145
|
-
connection =
|
|
156
|
+
connection = get_connection(connection_name)
|
|
146
157
|
|
|
147
158
|
connection.execute_wrappers = [
|
|
148
159
|
w
|
|
@@ -291,6 +302,9 @@ def setup_django(
|
|
|
291
302
|
django.db.connections._connections = threading.local()
|
|
292
303
|
logger.debug("django.db.connections._connections has been patched")
|
|
293
304
|
|
|
305
|
+
# error if trying to query with the default connection without setting up an instance
|
|
306
|
+
get_connection("default").execute_wrappers.insert(0, error_no_instance_wrapper)
|
|
307
|
+
|
|
294
308
|
if isettings._fine_grained_access and isettings._db_permissions == "jwt":
|
|
295
309
|
db_token = DBToken(isettings)
|
|
296
310
|
db_token_manager.set(db_token) # sets for the default connection
|
|
@@ -311,10 +325,24 @@ def setup_django(
|
|
|
311
325
|
call_command("migrate", app_name, app_number, verbosity=2)
|
|
312
326
|
isettings._update_cloud_sqlite_file(unlock_cloud_sqlite=False)
|
|
313
327
|
elif init:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
328
|
+
modules_beyond_bionty = isettings.modules.copy()
|
|
329
|
+
compressed_sqlite_path = Path(__file__).parent / "lamin.db.gz"
|
|
330
|
+
if "bionty" in modules_beyond_bionty:
|
|
331
|
+
modules_beyond_bionty.remove("bionty")
|
|
332
|
+
if (
|
|
333
|
+
isettings.dialect == "postgresql"
|
|
334
|
+
or os.getenv("LAMINDB_INIT_FROM_SCRATCH", "false") == "true"
|
|
335
|
+
or len(modules_beyond_bionty) > 0
|
|
336
|
+
or not compressed_sqlite_path.exists()
|
|
337
|
+
):
|
|
338
|
+
global IS_MIGRATING
|
|
339
|
+
IS_MIGRATING = True
|
|
340
|
+
call_command("migrate", verbosity=0)
|
|
341
|
+
IS_MIGRATING = False
|
|
342
|
+
else:
|
|
343
|
+
with gzip.open(compressed_sqlite_path, "rb") as f_in:
|
|
344
|
+
with open(isettings._sqlite_file_local, "wb") as f_out:
|
|
345
|
+
shutil.copyfileobj(f_in, f_out)
|
|
318
346
|
|
|
319
347
|
global IS_SETUP
|
|
320
348
|
IS_SETUP = True
|
|
Binary file
|
lamindb_setup/core/upath.py
CHANGED
|
@@ -1005,13 +1005,22 @@ def check_storage_is_empty(
|
|
|
1005
1005
|
objects = [o for o in objects if "/.lamindb/_exclusion/" not in o]
|
|
1006
1006
|
n_files = len(objects)
|
|
1007
1007
|
n_diff = n_files - n_offset_objects
|
|
1008
|
-
ask_for_deletion = (
|
|
1009
|
-
"delete them prior to deleting the storage location"
|
|
1010
|
-
if raise_error
|
|
1011
|
-
else "consider deleting them"
|
|
1012
|
-
)
|
|
1013
|
-
message = f"'{directory_string}' contains {n_diff} objects - {ask_for_deletion}"
|
|
1014
1008
|
if n_diff > 0:
|
|
1009
|
+
ask_for_deletion = (
|
|
1010
|
+
"delete them prior to deleting the storage location"
|
|
1011
|
+
if raise_error
|
|
1012
|
+
else "consider deleting them"
|
|
1013
|
+
)
|
|
1014
|
+
message = f"'{directory_string}' contains {n_diff} objects:\n"
|
|
1015
|
+
message += "\n".join(
|
|
1016
|
+
[
|
|
1017
|
+
o
|
|
1018
|
+
for o in objects
|
|
1019
|
+
if not o.endswith(".lamindb/storage_uid.txt")
|
|
1020
|
+
and not (account_for_sqlite_file and o.endswith(".lamindb/lamin.db"))
|
|
1021
|
+
]
|
|
1022
|
+
)
|
|
1023
|
+
message += f"\n{ask_for_deletion}"
|
|
1015
1024
|
if raise_error:
|
|
1016
1025
|
raise StorageNotEmpty(message) from None
|
|
1017
1026
|
else:
|
lamindb_setup/errors.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Errors.
|
|
2
2
|
|
|
3
3
|
.. autoexception:: CurrentInstanceNotConfigured
|
|
4
|
-
.. autoexception:: InstanceNotSetupError
|
|
5
4
|
.. autoexception:: ModuleWasntConfigured
|
|
6
5
|
.. autoexception:: StorageAlreadyManaged
|
|
7
6
|
.. autoexception:: StorageNotEmpty
|
|
@@ -25,17 +24,6 @@ class DefaultMessageException(Exception):
|
|
|
25
24
|
super().__init__(message)
|
|
26
25
|
|
|
27
26
|
|
|
28
|
-
# TODO: remove this exception sooner or later because we don't have a need for it anymore
|
|
29
|
-
class InstanceNotSetupError(DefaultMessageException):
|
|
30
|
-
default_message = """\
|
|
31
|
-
To use lamindb, you need to connect to an instance.
|
|
32
|
-
|
|
33
|
-
Connect to an instance: `ln.connect()`. Init an instance: `ln.setup.init()`.
|
|
34
|
-
|
|
35
|
-
If you used the CLI to set up lamindb in a notebook, restart the Python session.
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
27
|
class CurrentInstanceNotConfigured(DefaultMessageException):
|
|
40
28
|
default_message = """\
|
|
41
29
|
No instance is connected! Call
|
lamindb_setup/io.py
CHANGED
|
@@ -14,7 +14,7 @@ from django.db import models, transaction
|
|
|
14
14
|
from rich.progress import Progress
|
|
15
15
|
|
|
16
16
|
if TYPE_CHECKING:
|
|
17
|
-
from collections.abc import Iterable
|
|
17
|
+
from collections.abc import Iterable
|
|
18
18
|
from typing import Literal
|
|
19
19
|
|
|
20
20
|
|
|
@@ -22,6 +22,9 @@ def _get_registries(module_name: str) -> list[str]:
|
|
|
22
22
|
"""Get registry class names from a module."""
|
|
23
23
|
schema_module = import_module(module_name)
|
|
24
24
|
|
|
25
|
+
# Ensure that models are loaded; we've observed empty exports otherwise
|
|
26
|
+
from django.db import models
|
|
27
|
+
|
|
25
28
|
return [
|
|
26
29
|
name
|
|
27
30
|
for name in dir(schema_module.models)
|
|
@@ -142,9 +145,9 @@ def _export_full_table(
|
|
|
142
145
|
|
|
143
146
|
|
|
144
147
|
def export_db(
|
|
145
|
-
module_names:
|
|
148
|
+
module_names: Iterable[str] | None = None,
|
|
146
149
|
*,
|
|
147
|
-
output_dir: str | Path =
|
|
150
|
+
output_dir: str | Path | None = None,
|
|
148
151
|
max_workers: int = 8,
|
|
149
152
|
chunk_size: int = 500_000,
|
|
150
153
|
) -> None:
|
|
@@ -159,6 +162,11 @@ def export_db(
|
|
|
159
162
|
max_workers: Number of parallel processes.
|
|
160
163
|
chunk_size: Number of rows per chunk for large tables.
|
|
161
164
|
"""
|
|
165
|
+
import lamindb_setup as ln_setup
|
|
166
|
+
|
|
167
|
+
if output_dir is None:
|
|
168
|
+
output_dir = f"./{ln_setup.settings.instance.name}_export/"
|
|
169
|
+
|
|
162
170
|
directory = Path(output_dir)
|
|
163
171
|
directory.mkdir(parents=True, exist_ok=True)
|
|
164
172
|
|
|
@@ -332,12 +340,15 @@ def import_db(
|
|
|
332
340
|
Temporarily disables FK constraints to allow insertion in arbitrary order.
|
|
333
341
|
Requires superuser/RDS admin privileges for postgres databases.
|
|
334
342
|
|
|
343
|
+
Note: When running in a subprocess, add a short delay or explicit connection close after `import_db()`
|
|
344
|
+
to ensure all SQLite writes are flushed to disk before process termination.
|
|
345
|
+
|
|
335
346
|
Args:
|
|
336
347
|
input_dir: Directory containing parquet files to import.
|
|
337
348
|
module_names: Module names to import (e.g., ["lamindb", "bionty", "wetlab"]).
|
|
338
349
|
if_exists: How to behave if table exists: 'fail', 'replace', or 'append'.
|
|
339
|
-
If set to 'replace', existing data is deleted and new data is imported. PKs and indices are not guaranteed to be preserved which can lead to write errors.
|
|
340
|
-
If set to 'append', new data is added to existing data without clearing the table. PKs and indices are preserved but database size will greatly increase.
|
|
350
|
+
If set to 'replace', existing data is deleted and new data is imported. All PKs and indices are not guaranteed to be preserved which can lead to write errors.
|
|
351
|
+
If set to 'append', new data is added to existing data without clearing the table. All PKs and indices are preserved allowing write operations but database size will greatly increase.
|
|
341
352
|
If set to 'fail', raises an error if the table contains any data.
|
|
342
353
|
"""
|
|
343
354
|
from django.db import connection
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lamindb_setup
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.17.0
|
|
4
4
|
Summary: Setup & configure LaminDB.
|
|
5
5
|
Author-email: Lamin Labs <open-source@lamin.ai>
|
|
6
6
|
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
8
9
|
Requires-Dist: lamin_utils>=0.3.3
|
|
9
10
|
Requires-Dist: django>=5.2,<5.3
|
|
10
11
|
Requires-Dist: dj_database_url>=1.3.0,<3.0.0
|
|
@@ -15,9 +16,7 @@ Requires-Dist: httpx_retries<1.0.0
|
|
|
15
16
|
Requires-Dist: requests
|
|
16
17
|
Requires-Dist: universal_pathlib==0.2.6
|
|
17
18
|
Requires-Dist: botocore<2.0.0
|
|
18
|
-
Requires-Dist: supabase>=2.
|
|
19
|
-
Requires-Dist: gotrue<=2.12.0
|
|
20
|
-
Requires-Dist: storage3!=0.11.2; python_version < '3.11'
|
|
19
|
+
Requires-Dist: supabase>=2.20.0,<=2.24.0
|
|
21
20
|
Requires-Dist: pyjwt<3.0.0
|
|
22
21
|
Requires-Dist: psutil
|
|
23
22
|
Requires-Dist: packaging
|
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
lamindb_setup/__init__.py,sha256=
|
|
1
|
+
lamindb_setup/__init__.py,sha256=MW5TlycTeavLeMWfllbDgMo2z4r7zlNHHvMLEaJRnGE,3270
|
|
2
2
|
lamindb_setup/_cache.py,sha256=pGvDNVHGx4HWr_6w5ajqEJOdysmaGc6F221qFnXkT-k,2747
|
|
3
3
|
lamindb_setup/_check.py,sha256=28PcG8Kp6OpjSLSi1r2boL2Ryeh6xkaCL87HFbjs6GA,129
|
|
4
|
-
lamindb_setup/_check_setup.py,sha256=
|
|
5
|
-
lamindb_setup/_connect_instance.py,sha256=
|
|
6
|
-
lamindb_setup/_delete.py,sha256=
|
|
7
|
-
lamindb_setup/_disconnect.py,sha256=
|
|
4
|
+
lamindb_setup/_check_setup.py,sha256=zPQho12dctJYjnZxpumq2r7XRvXwrNyWi5-UMRPSeuE,4297
|
|
5
|
+
lamindb_setup/_connect_instance.py,sha256=AVJ5wDGsvPO1HAWw2KIz0R2hF0c9DMLJ_2kFsXJfwd4,17156
|
|
6
|
+
lamindb_setup/_delete.py,sha256=ZMAik35NNcj4kDfOISDfzpicCi4j8gwmWAor0hEB-Jk,6123
|
|
7
|
+
lamindb_setup/_disconnect.py,sha256=B0K0yTIIyhwc3MzDFL_-cKutm-_DHsiJSTrkp1MMyXA,1470
|
|
8
8
|
lamindb_setup/_django.py,sha256=uIQflpkp8l3axyPaKURlk3kacgpElVP5KOKmFxYSMGk,1454
|
|
9
9
|
lamindb_setup/_entry_points.py,sha256=sKwXPX9xjOotoAjvgkU5LBwjjHLWVkh0ZGdiSsrch9k,522
|
|
10
|
-
lamindb_setup/_init_instance.py,sha256=
|
|
11
|
-
lamindb_setup/_migrate.py,sha256=
|
|
10
|
+
lamindb_setup/_init_instance.py,sha256=EBdv0ga_nnqHU-m9fN7EkVg7YEuKOLxR8N87Ww2qEDg,14877
|
|
11
|
+
lamindb_setup/_migrate.py,sha256=B7dRb4hIkuHlRxmI25N7xkdQve-fNZDZoq4GsP3TtH0,10512
|
|
12
12
|
lamindb_setup/_register_instance.py,sha256=RdUZxZWHLdbvdNZWpF8e0UWROb_T0cStWbzc5yUw34I,1047
|
|
13
13
|
lamindb_setup/_schema.py,sha256=b3uzhhWpV5mQtDwhMINc2MabGCnGLESy51ito3yl6Wc,679
|
|
14
14
|
lamindb_setup/_schema_metadata.py,sha256=Whs-e4ZMnA1niZ2l5Eu8il-33IxI4Hr5ylGEgPxx8wk,15628
|
|
15
15
|
lamindb_setup/_set_managed_storage.py,sha256=xQe5DXCRiQ5VseAjVC2Bki0wB0n0tSTchvVKSx9I6eo,3094
|
|
16
|
-
lamindb_setup/_setup_user.py,sha256=
|
|
17
|
-
lamindb_setup/_silence_loggers.py,sha256=
|
|
18
|
-
lamindb_setup/errors.py,sha256=
|
|
19
|
-
lamindb_setup/io.py,sha256=
|
|
16
|
+
lamindb_setup/_setup_user.py,sha256=9CwKUru2jza1vXu4A5V-q-oXnbIXwR_YwjKbVnETK7Q,6931
|
|
17
|
+
lamindb_setup/_silence_loggers.py,sha256=oQnvnvieqxARQQMwQkR82EJVk1TaM-Bo9o0UCOsCcXY,1697
|
|
18
|
+
lamindb_setup/errors.py,sha256=hELv-tJSjyPDqT9pZ_-tmg6q_wXDtAgn8m1fIvFeMtg,1528
|
|
19
|
+
lamindb_setup/io.py,sha256=stTE6VHCIpLK1ueRuWEJGh35wo9Qd1NcNN1TnLEbK-U,17156
|
|
20
20
|
lamindb_setup/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
lamindb_setup/types.py,sha256=fuQxZJnrGYe7a_Ju9n1RqO-HhkOAr1l1xjpAg9dmBu8,605
|
|
22
22
|
lamindb_setup/core/__init__.py,sha256=adZtacDwG2C0tgx-ypp9yOAqw9qaR-IRWkgLurKpXVE,668
|
|
23
|
-
lamindb_setup/core/_aws_options.py,sha256=
|
|
23
|
+
lamindb_setup/core/_aws_options.py,sha256=5XbcPtRU_oVrfbuG_0kngyYfxswLLnRS3nKO4XEWepU,10054
|
|
24
24
|
lamindb_setup/core/_aws_storage.py,sha256=QEtV-riQrwfivcwqHnXBbkJ-9YyNEXL4fLoCmOHZ1BI,2003
|
|
25
25
|
lamindb_setup/core/_clone.py,sha256=oLTMItGxRQB1THSDP-RG2eH1qPTVcuZ7_-eElttJ518,6451
|
|
26
26
|
lamindb_setup/core/_deprecated.py,sha256=M3vpM4fZPOncxY2qsXQAPeaEph28xWdv7tYaueaUyAA,2554
|
|
27
27
|
lamindb_setup/core/_docs.py,sha256=3k-YY-oVaJd_9UIY-LfBg_u8raKOCNfkZQPA73KsUhs,276
|
|
28
|
-
lamindb_setup/core/_hub_client.py,sha256=
|
|
29
|
-
lamindb_setup/core/_hub_core.py,sha256=
|
|
28
|
+
lamindb_setup/core/_hub_client.py,sha256=113SKT3Ki4KxyKqpYOwIB8mEG0ouy9oebLJoh_xGJZY,10426
|
|
29
|
+
lamindb_setup/core/_hub_core.py,sha256=29iPGnmV1hzYIMrErgwJoHwTQ5RHNJ6-icDM81aC4ac,29255
|
|
30
30
|
lamindb_setup/core/_hub_crud.py,sha256=j6516H82kLjFUNPqFGUINbDw9YbofMgjxadGzYb0OS4,6362
|
|
31
31
|
lamindb_setup/core/_hub_utils.py,sha256=6dyDGyzYFgVfR_lE3VN3CP1jGp98gxPtr-T91PAP05U,2687
|
|
32
32
|
lamindb_setup/core/_private_django_api.py,sha256=Z9uGL4CK0OX58rc8R_qarg9rIBp1DgjsjfP9Vj2vJHI,2629
|
|
33
|
-
lamindb_setup/core/_settings.py,sha256=
|
|
34
|
-
lamindb_setup/core/_settings_instance.py,sha256=
|
|
35
|
-
lamindb_setup/core/_settings_load.py,sha256=
|
|
33
|
+
lamindb_setup/core/_settings.py,sha256=LTeh2jsM01GHW2ZTrcc6rXNz6qIUT0VPEzpE9I8tg3Q,15497
|
|
34
|
+
lamindb_setup/core/_settings_instance.py,sha256=EkKcT5TcGLIiMQdiTNCrf8OPp0wmj1ZmY_9kL7ovhZc,24994
|
|
35
|
+
lamindb_setup/core/_settings_load.py,sha256=D6r0fiqJznHhTN_apGlrKfYzl0g21shIaV4z4zEr7Z8,5780
|
|
36
36
|
lamindb_setup/core/_settings_save.py,sha256=96mWdYLyfvbnG_ok_vK4x7jm-rtqcWCD1OHEt2QSAms,3328
|
|
37
37
|
lamindb_setup/core/_settings_storage.py,sha256=22EBagIr5qOZr9pqVkJsTcQtgE14en-Wh0y9rgF4FEQ,15677
|
|
38
38
|
lamindb_setup/core/_settings_store.py,sha256=auZssUBb6qE5oSqdGiHhqI2B46qSpegX89VwObPQksk,2601
|
|
39
39
|
lamindb_setup/core/_settings_user.py,sha256=gFfyMf-738onbh1Mf4wsmLlenQJPtjQfpUgKnOlqc2o,1453
|
|
40
40
|
lamindb_setup/core/_setup_bionty_sources.py,sha256=ox3X-SHiHa2lNPSWjwZhINypbLacX6kGwH6hVVrSFZc,1505
|
|
41
41
|
lamindb_setup/core/cloud_sqlite_locker.py,sha256=H_CTUCjURFXwD1cCtV_Jn0_60iztZTkaesLLXIBgIxc,7204
|
|
42
|
-
lamindb_setup/core/django.py,sha256=
|
|
42
|
+
lamindb_setup/core/django.py,sha256=JTlGZ2I9EGTr49NjLMTHY1FfjkOoqcoYNgMQitQRGyQ,13640
|
|
43
43
|
lamindb_setup/core/exceptions.py,sha256=qjMzqy_uzPA7mCOdnoWnS_fdA6OWbdZGftz-YYplrY0,84
|
|
44
44
|
lamindb_setup/core/hashing.py,sha256=Y8Uc5uSGTfU6L2R_gb5w8DdHhGRog7RnkK-e9FEMjPY,3680
|
|
45
|
+
lamindb_setup/core/lamin.db.gz,sha256=cSWQ_ak6QN-SMyFA4x6zv-e2rNXr1IOrhDU5AfVAADc,81394
|
|
45
46
|
lamindb_setup/core/types.py,sha256=T7NwspfRHgIIpYsXDcApks8jkOlGeGRW-YbVLB7jNIo,67
|
|
46
|
-
lamindb_setup/core/upath.py,sha256=
|
|
47
|
-
lamindb_setup-1.
|
|
48
|
-
lamindb_setup-1.
|
|
49
|
-
lamindb_setup-1.
|
|
50
|
-
lamindb_setup-1.
|
|
47
|
+
lamindb_setup/core/upath.py,sha256=zlPZcDRHDYT7OcoOiX_w9koAzfBr9q8hB66OPwhTpBo,36504
|
|
48
|
+
lamindb_setup-1.17.0.dist-info/licenses/LICENSE,sha256=UOZ1F5fFDe3XXvG4oNnkL1-Ecun7zpHzRxjp-XsMeAo,11324
|
|
49
|
+
lamindb_setup-1.17.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
50
|
+
lamindb_setup-1.17.0.dist-info/METADATA,sha256=Q8iSdBECnZFm2PKkraif0zjcwm5CqSem3NW0UdCPBHo,1766
|
|
51
|
+
lamindb_setup-1.17.0.dist-info/RECORD,,
|
|
File without changes
|