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,350 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import secrets
|
|
5
|
+
import shutil
|
|
6
|
+
import string
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
9
|
+
|
|
10
|
+
from lamin_utils import logger
|
|
11
|
+
|
|
12
|
+
from ._aws_credentials import HOSTED_REGIONS, get_aws_credentials_manager
|
|
13
|
+
from ._aws_storage import find_closest_aws_region
|
|
14
|
+
from .upath import (
|
|
15
|
+
LocalPathClasses,
|
|
16
|
+
UPath,
|
|
17
|
+
create_path,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from uuid import UUID
|
|
22
|
+
|
|
23
|
+
from .types import UPathStr
|
|
24
|
+
|
|
25
|
+
IS_INITIALIZED_KEY = ".lamindb/_is_initialized"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def base62(n_char: int) -> str:
|
|
29
|
+
"""Like nanoid without hyphen and underscore."""
|
|
30
|
+
alphabet = string.digits + string.ascii_letters.swapcase()
|
|
31
|
+
id = "".join(secrets.choice(alphabet) for i in range(n_char))
|
|
32
|
+
return id
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_storage_region(path: UPathStr) -> str | None:
|
|
36
|
+
path_str = str(path)
|
|
37
|
+
if path_str.startswith("s3://"):
|
|
38
|
+
import botocore.session
|
|
39
|
+
from botocore.config import Config
|
|
40
|
+
from botocore.exceptions import ClientError
|
|
41
|
+
|
|
42
|
+
# strip the prefix and any suffixes of the bucket name
|
|
43
|
+
bucket = path_str.replace("s3://", "").split("/")[0]
|
|
44
|
+
session = botocore.session.get_session()
|
|
45
|
+
credentials = session.get_credentials()
|
|
46
|
+
if credentials is None or credentials.access_key is None:
|
|
47
|
+
config = Config(signature_version=botocore.session.UNSIGNED)
|
|
48
|
+
else:
|
|
49
|
+
config = None
|
|
50
|
+
s3_client = session.create_client("s3", config=config)
|
|
51
|
+
try:
|
|
52
|
+
response = s3_client.head_bucket(Bucket=bucket)
|
|
53
|
+
except ClientError as exc:
|
|
54
|
+
response = getattr(exc, "response", {})
|
|
55
|
+
if response.get("Error", {}).get("Code") == "404":
|
|
56
|
+
raise exc
|
|
57
|
+
region = (
|
|
58
|
+
response.get("ResponseMetadata", {})
|
|
59
|
+
.get("HTTPHeaders", {})
|
|
60
|
+
.get("x-amz-bucket-region")
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
region = None
|
|
64
|
+
return region
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def mark_storage_root(root: UPathStr, uid: str):
|
|
68
|
+
# we need to touch a 0-byte object in folder-like storage location on S3 to avoid
|
|
69
|
+
# permission errors from leveraging s3fs on an empty hosted storage location
|
|
70
|
+
# for consistency, we write this file everywhere
|
|
71
|
+
root_upath = UPath(root)
|
|
72
|
+
mark_upath = root_upath / IS_INITIALIZED_KEY
|
|
73
|
+
mark_upath.write_text(uid)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def init_storage(
|
|
77
|
+
root: UPathStr,
|
|
78
|
+
instance_id: UUID | None = None,
|
|
79
|
+
register_hub: bool | None = None,
|
|
80
|
+
prevent_register_hub: bool = False,
|
|
81
|
+
init_instance: bool = False,
|
|
82
|
+
created_by: UUID | None = None,
|
|
83
|
+
access_token: str | None = None,
|
|
84
|
+
) -> tuple[
|
|
85
|
+
StorageSettings,
|
|
86
|
+
Literal["hub-record-not-created", "hub-record-retireved", "hub-record-created"],
|
|
87
|
+
]:
|
|
88
|
+
if root is None:
|
|
89
|
+
raise ValueError("`storage` argument can't be `None`")
|
|
90
|
+
root_str = str(root) # ensure we have a string
|
|
91
|
+
if ".lamindb" in root_str:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
'Please pass a folder name that does not end or contain ".lamindb"'
|
|
94
|
+
)
|
|
95
|
+
uid = os.getenv("LAMINDB_STORAGE_LNID_INIT")
|
|
96
|
+
if uid is None:
|
|
97
|
+
uid = base62(12)
|
|
98
|
+
else:
|
|
99
|
+
# this means we constructed a hosted location of shape s3://bucket-name/uid
|
|
100
|
+
# within LaminHub
|
|
101
|
+
assert root_str.endswith(uid)
|
|
102
|
+
region = None
|
|
103
|
+
lamin_env = os.getenv("LAMIN_ENV")
|
|
104
|
+
if root_str.startswith("create-s3"):
|
|
105
|
+
if root_str != "create-s3":
|
|
106
|
+
assert "--" in root_str, "example: `create-s3--eu-central-1`"
|
|
107
|
+
region = root_str.replace("create-s3--", "")
|
|
108
|
+
if region is None:
|
|
109
|
+
region = find_closest_aws_region()
|
|
110
|
+
else:
|
|
111
|
+
if region not in HOSTED_REGIONS:
|
|
112
|
+
raise ValueError(f"region has to be one of {HOSTED_REGIONS}")
|
|
113
|
+
if lamin_env is None or lamin_env == "prod":
|
|
114
|
+
root_str = f"s3://lamin-{region}/{uid}"
|
|
115
|
+
else:
|
|
116
|
+
root_str = f"s3://lamin-hosted-test/{uid}"
|
|
117
|
+
elif root_str.startswith(("gs://", "s3://")):
|
|
118
|
+
pass
|
|
119
|
+
else: # local path
|
|
120
|
+
try:
|
|
121
|
+
_ = Path(root_str)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
logger.error("`storage` is not a valid local, GCP storage or AWS S3 path")
|
|
124
|
+
raise e
|
|
125
|
+
ssettings = StorageSettings(
|
|
126
|
+
uid=uid,
|
|
127
|
+
root=root_str,
|
|
128
|
+
region=region,
|
|
129
|
+
instance_id=instance_id,
|
|
130
|
+
access_token=access_token,
|
|
131
|
+
)
|
|
132
|
+
# this stores the result of init_storage_hub
|
|
133
|
+
hub_record_status: Literal[
|
|
134
|
+
"hub-record-not-created", "hub-record-retireved", "hub-record-created"
|
|
135
|
+
] = "hub-record-not-created"
|
|
136
|
+
# the below might update the uid with one that's already taken on the hub
|
|
137
|
+
if not prevent_register_hub:
|
|
138
|
+
if ssettings.type_is_cloud or register_hub:
|
|
139
|
+
from ._hub_core import delete_storage_record
|
|
140
|
+
from ._hub_core import init_storage as init_storage_hub
|
|
141
|
+
|
|
142
|
+
hub_record_status = init_storage_hub(
|
|
143
|
+
ssettings,
|
|
144
|
+
auto_populate_instance=not init_instance,
|
|
145
|
+
created_by=created_by,
|
|
146
|
+
access_token=access_token,
|
|
147
|
+
)
|
|
148
|
+
# below comes last only if everything else was successful
|
|
149
|
+
try:
|
|
150
|
+
# (federated) credentials for AWS access are provisioned under-the-hood
|
|
151
|
+
# discussion: https://laminlabs.slack.com/archives/C04FPE8V01W/p1719260587167489
|
|
152
|
+
# if access_token was passed in ssettings, it is used here
|
|
153
|
+
mark_storage_root(ssettings.root, ssettings.uid) # type: ignore
|
|
154
|
+
except Exception:
|
|
155
|
+
logger.important(
|
|
156
|
+
f"due to lack of write access, LaminDB won't manage storage location: {ssettings.root}"
|
|
157
|
+
)
|
|
158
|
+
# we have to check hub_record_status here because
|
|
159
|
+
# _select_storage inside init_storage_hub also populates ssettings._uuid
|
|
160
|
+
# and we don't want to delete an existing storage record here
|
|
161
|
+
# only newly created
|
|
162
|
+
if hub_record_status == "hub-record-created" and ssettings._uuid is not None:
|
|
163
|
+
delete_storage_record(ssettings._uuid, access_token=access_token) # type: ignore
|
|
164
|
+
ssettings._instance_id = None
|
|
165
|
+
return ssettings, hub_record_status
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class StorageSettings:
|
|
169
|
+
"""Settings for a given storage location (local or cloud)."""
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
root: UPathStr,
|
|
174
|
+
region: str | None = None,
|
|
175
|
+
uid: str | None = None,
|
|
176
|
+
uuid: UUID | None = None,
|
|
177
|
+
instance_id: UUID | None = None,
|
|
178
|
+
# note that passing access_token prevents credentials caching
|
|
179
|
+
access_token: str | None = None,
|
|
180
|
+
):
|
|
181
|
+
self._uid = uid
|
|
182
|
+
self._uuid_ = uuid
|
|
183
|
+
self._root_init = UPath(root)
|
|
184
|
+
if isinstance(self._root_init, LocalPathClasses): # local paths
|
|
185
|
+
try:
|
|
186
|
+
(self._root_init / ".lamindb").mkdir(parents=True, exist_ok=True)
|
|
187
|
+
self._root_init = self._root_init.resolve()
|
|
188
|
+
except Exception:
|
|
189
|
+
logger.warning(f"unable to create .lamindb folder in {self._root_init}")
|
|
190
|
+
pass
|
|
191
|
+
self._root = None
|
|
192
|
+
self._instance_id = instance_id
|
|
193
|
+
# we don't yet infer region here to make init fast
|
|
194
|
+
self._region = region
|
|
195
|
+
# would prefer to type below as Registry, but need to think through import order
|
|
196
|
+
self._record: Any | None = None
|
|
197
|
+
# save access_token here for use in self.root
|
|
198
|
+
self.access_token = access_token
|
|
199
|
+
|
|
200
|
+
# local storage
|
|
201
|
+
self._has_local = False
|
|
202
|
+
self._local = None
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def id(self) -> int:
|
|
206
|
+
"""Storage id in current instance."""
|
|
207
|
+
return self.record.id
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def _uuid(self) -> UUID | None:
|
|
211
|
+
"""Lamin's internal storage uuid."""
|
|
212
|
+
return self._uuid_
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def uid(self) -> str | None:
|
|
216
|
+
"""Storage id."""
|
|
217
|
+
if self._uid is None:
|
|
218
|
+
self._uid = self.record.uid
|
|
219
|
+
return self._uid
|
|
220
|
+
|
|
221
|
+
@property
|
|
222
|
+
def _mark_storage_root(self) -> UPath:
|
|
223
|
+
return self.root / IS_INITIALIZED_KEY
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def record(self) -> Any:
|
|
227
|
+
"""Storage record in current instance."""
|
|
228
|
+
if self._record is None:
|
|
229
|
+
# dynamic import because of import order
|
|
230
|
+
from lnschema_core.models import Storage
|
|
231
|
+
|
|
232
|
+
from ._settings import settings
|
|
233
|
+
|
|
234
|
+
self._record = Storage.objects.using(settings._using_key).get(
|
|
235
|
+
root=self.root_as_str
|
|
236
|
+
)
|
|
237
|
+
return self._record
|
|
238
|
+
|
|
239
|
+
def __repr__(self):
|
|
240
|
+
"""String rep."""
|
|
241
|
+
s = f"root='{self.root_as_str}', uid='{self.uid}'"
|
|
242
|
+
if self._uuid is not None:
|
|
243
|
+
s += f", uuid='{self._uuid.hex}'"
|
|
244
|
+
return f"StorageSettings({s})"
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def root(self) -> UPath:
|
|
248
|
+
"""Root storage location."""
|
|
249
|
+
if self._root is None:
|
|
250
|
+
# below makes network requests to get credentials
|
|
251
|
+
self._root = create_path(self._root_init, access_token=self.access_token)
|
|
252
|
+
elif getattr(self._root, "protocol", "") == "s3":
|
|
253
|
+
# this is needed to be sure that the root always has nonexpired credentials
|
|
254
|
+
# this just checks for time of the cached credentials in most cases
|
|
255
|
+
return get_aws_credentials_manager().enrich_path(
|
|
256
|
+
self._root, access_token=self.access_token
|
|
257
|
+
)
|
|
258
|
+
return self._root
|
|
259
|
+
|
|
260
|
+
def _set_fs_kwargs(self, **kwargs):
|
|
261
|
+
"""Set additional fsspec arguments for cloud root.
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
|
|
265
|
+
>>> ln.setup.settings.storage._set_fs_kwargs( # any fsspec args
|
|
266
|
+
>>> profile="some_profile", cache_regions=True
|
|
267
|
+
>>> )
|
|
268
|
+
"""
|
|
269
|
+
if not isinstance(self._root, LocalPathClasses) and kwargs != {}:
|
|
270
|
+
self._root = UPath(self.root, **kwargs)
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def root_as_str(self) -> str:
|
|
274
|
+
"""Formatted root string."""
|
|
275
|
+
return self._root_init.as_posix().rstrip("/")
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def cache_dir(
|
|
279
|
+
self,
|
|
280
|
+
) -> UPath:
|
|
281
|
+
"""Cache root, a local directory to cache cloud files."""
|
|
282
|
+
from lamindb_setup import settings
|
|
283
|
+
|
|
284
|
+
return settings.cache_dir
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def type_is_cloud(self) -> bool:
|
|
288
|
+
"""`True` if `storage_root` is in cloud, `False` otherwise."""
|
|
289
|
+
return self.type != "local"
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def region(self) -> str | None:
|
|
293
|
+
"""Storage region."""
|
|
294
|
+
if self._region is None:
|
|
295
|
+
self._region = get_storage_region(self.root_as_str)
|
|
296
|
+
return self._region
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def type(self) -> Literal["local", "s3", "gs"]:
|
|
300
|
+
"""AWS S3 vs. Google Cloud vs. local.
|
|
301
|
+
|
|
302
|
+
Returns the protocol as a string: "local", "s3", "gs".
|
|
303
|
+
"""
|
|
304
|
+
import fsspec
|
|
305
|
+
|
|
306
|
+
convert = {"file": "local"}
|
|
307
|
+
protocol = fsspec.utils.get_protocol(self.root_as_str)
|
|
308
|
+
return convert.get(protocol, protocol) # type: ignore
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def is_on_hub(self) -> bool:
|
|
312
|
+
"""Is this instance on the hub.
|
|
313
|
+
|
|
314
|
+
Only works if user has access to the instance.
|
|
315
|
+
"""
|
|
316
|
+
if self._uuid is None:
|
|
317
|
+
return False
|
|
318
|
+
else:
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
def cloud_to_local(
|
|
322
|
+
self, filepath: UPathStr, cache_key: str | None = None, **kwargs
|
|
323
|
+
) -> UPath:
|
|
324
|
+
"""Local (or local cache) filepath from filepath."""
|
|
325
|
+
# cache_key is ignored in cloud_to_local_no_update if filepath is local
|
|
326
|
+
local_filepath = self.cloud_to_local_no_update(filepath, cache_key)
|
|
327
|
+
if isinstance(filepath, UPath) and not isinstance(filepath, LocalPathClasses):
|
|
328
|
+
local_filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
329
|
+
filepath.synchronize(local_filepath, **kwargs)
|
|
330
|
+
return local_filepath
|
|
331
|
+
|
|
332
|
+
def cloud_to_local_no_update(
|
|
333
|
+
self, filepath: UPathStr, cache_key: str | None = None
|
|
334
|
+
) -> UPath:
|
|
335
|
+
# cache_key is ignored if filepath is local
|
|
336
|
+
if isinstance(filepath, UPath) and not isinstance(filepath, LocalPathClasses):
|
|
337
|
+
local_filepath = self.cache_dir / (
|
|
338
|
+
filepath.path if cache_key is None else cache_key
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
local_filepath = filepath
|
|
342
|
+
return UPath(local_filepath)
|
|
343
|
+
|
|
344
|
+
def key_to_filepath(self, filekey: UPathStr) -> UPath:
|
|
345
|
+
"""Cloud or local filepath from filekey."""
|
|
346
|
+
return self.root / filekey
|
|
347
|
+
|
|
348
|
+
def local_filepath(self, filekey: UPathStr) -> UPath:
|
|
349
|
+
"""Local (cache) filepath from filekey: `local(filepath(...))`."""
|
|
350
|
+
return self.cloud_to_local(self.key_to_filepath(filekey))
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
if "LAMIN_SETTINGS_DIR" in os.environ:
|
|
8
|
+
# Needed when running with AWS Lambda, as only tmp/ directory has a write access
|
|
9
|
+
settings_dir = Path(f"{os.environ['LAMIN_SETTINGS_DIR']}/.lamin")
|
|
10
|
+
else:
|
|
11
|
+
# user_config_dir in appdirs is weird on MacOS!
|
|
12
|
+
# hence, let's take home/.lamin
|
|
13
|
+
settings_dir = Path.home() / ".lamin"
|
|
14
|
+
|
|
15
|
+
settings_dir.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_settings_file_name_prefix():
|
|
19
|
+
if "LAMIN_ENV" in os.environ:
|
|
20
|
+
if os.environ["LAMIN_ENV"] != "prod":
|
|
21
|
+
return f"{os.environ['LAMIN_ENV']}--"
|
|
22
|
+
return ""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def current_instance_settings_file():
|
|
26
|
+
return settings_dir / f"{get_settings_file_name_prefix()}current_instance.env"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def current_user_settings_file():
|
|
30
|
+
return settings_dir / f"{get_settings_file_name_prefix()}current_user.env"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def instance_settings_file(name: str, owner: str):
|
|
34
|
+
return (
|
|
35
|
+
settings_dir / f"{get_settings_file_name_prefix()}instance--{owner}--{name}.env"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def user_settings_file_email(email: str):
|
|
40
|
+
return settings_dir / f"{get_settings_file_name_prefix()}user--{email}.env"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def user_settings_file_handle(handle: str):
|
|
44
|
+
return settings_dir / f"{get_settings_file_name_prefix()}user--{handle}.env"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def system_storage_settings_file():
|
|
48
|
+
return settings_dir / "storage.env"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class InstanceSettingsStore(BaseSettings):
|
|
52
|
+
api_url: Optional[str] = None
|
|
53
|
+
owner: str
|
|
54
|
+
name: str
|
|
55
|
+
storage_root: str
|
|
56
|
+
storage_region: Optional[str] # take old type annotations here because pydantic
|
|
57
|
+
db: Optional[str] # doesn't like new types on 3.9 even with future annotations
|
|
58
|
+
schema_str: Optional[str]
|
|
59
|
+
schema_id: Optional[str] = None
|
|
60
|
+
id: str
|
|
61
|
+
git_repo: Optional[str]
|
|
62
|
+
keep_artifacts_local: Optional[bool]
|
|
63
|
+
model_config = SettingsConfigDict(env_prefix="lamindb_instance_", env_file=".env")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class UserSettingsStore(BaseSettings):
|
|
67
|
+
email: str
|
|
68
|
+
password: str
|
|
69
|
+
access_token: str
|
|
70
|
+
api_key: Optional[str] = None
|
|
71
|
+
uid: str
|
|
72
|
+
uuid: str
|
|
73
|
+
handle: str
|
|
74
|
+
name: str
|
|
75
|
+
model_config = SettingsConfigDict(env_prefix="lamin_user_", env_file=".env")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class user_description:
|
|
11
|
+
email = """User email."""
|
|
12
|
+
password = """API key or legacy password."""
|
|
13
|
+
uid = """Universal user ID."""
|
|
14
|
+
handle = "Unique handle."
|
|
15
|
+
name = "Full name."
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class UserSettings:
|
|
20
|
+
"""User data. All synched from cloud."""
|
|
21
|
+
|
|
22
|
+
handle: str = "anonymous"
|
|
23
|
+
"""Unique handle."""
|
|
24
|
+
email: str | None = None
|
|
25
|
+
"""User email."""
|
|
26
|
+
api_key: str | None = None
|
|
27
|
+
"""Beta API key."""
|
|
28
|
+
password: str | None = None
|
|
29
|
+
"""API key or legacy password."""
|
|
30
|
+
access_token: str | None = None
|
|
31
|
+
"""User access token."""
|
|
32
|
+
uid: str = "null"
|
|
33
|
+
"""Universal user ID."""
|
|
34
|
+
_uuid: UUID | None = None
|
|
35
|
+
"""Lamin's internal user ID."""
|
|
36
|
+
name: str | None = None
|
|
37
|
+
"""Full name."""
|
|
38
|
+
|
|
39
|
+
def __repr__(self) -> str:
|
|
40
|
+
"""Rich string representation."""
|
|
41
|
+
representation = f"Current user: {self.handle}"
|
|
42
|
+
attrs = ["handle", "email", "uid"]
|
|
43
|
+
for attr in attrs:
|
|
44
|
+
value = getattr(self, attr)
|
|
45
|
+
representation += f"\n- {attr}: {value}"
|
|
46
|
+
return representation
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def id(self):
|
|
50
|
+
"""Integer id valid in current intance."""
|
|
51
|
+
from lnschema_core.users import current_user_id
|
|
52
|
+
|
|
53
|
+
# there is no cache needed here because current_user_id()
|
|
54
|
+
# has its own cache
|
|
55
|
+
return current_user_id()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from django.db.utils import OperationalError, ProgrammingError
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ._settings_instance import InstanceSettings
|
|
9
|
+
|
|
10
|
+
# bionty.Source -> bionty.base
|
|
11
|
+
RENAME = {"name": "source", "description": "source_name"}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write_bionty_sources(isettings: InstanceSettings) -> None:
|
|
15
|
+
"""Write bionty sources to Source table."""
|
|
16
|
+
if "bionty" not in isettings.schema:
|
|
17
|
+
return None
|
|
18
|
+
import shutil
|
|
19
|
+
|
|
20
|
+
import bionty
|
|
21
|
+
import bionty.base as bionty_base
|
|
22
|
+
from bionty._bionty import list_biorecord_models
|
|
23
|
+
from bionty.base.dev._handle_sources import parse_sources_yaml
|
|
24
|
+
from bionty.models import Source
|
|
25
|
+
|
|
26
|
+
bionty_models = list_biorecord_models(bionty)
|
|
27
|
+
|
|
28
|
+
shutil.copy(
|
|
29
|
+
bionty_base.settings.current_sources, bionty_base.settings.lamindb_sources
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
all_sources = parse_sources_yaml(bionty_base.settings.local_sources)
|
|
33
|
+
all_sources_dict = all_sources.to_dict(orient="records")
|
|
34
|
+
|
|
35
|
+
def _get_currently_used(key: str):
|
|
36
|
+
return (
|
|
37
|
+
bionty_base.display_currently_used_sources()
|
|
38
|
+
.reset_index()
|
|
39
|
+
.set_index(["entity", key])
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
currently_used = _get_currently_used("organism")
|
|
43
|
+
|
|
44
|
+
all_records = []
|
|
45
|
+
for kwargs in all_sources_dict:
|
|
46
|
+
act = currently_used.loc[(kwargs["entity"], kwargs["organism"])].to_dict()
|
|
47
|
+
if (act["source"] == kwargs["source"]) and (
|
|
48
|
+
act["version"] == kwargs["version"]
|
|
49
|
+
):
|
|
50
|
+
kwargs["currently_used"] = True
|
|
51
|
+
|
|
52
|
+
# when the database is not yet migrated but setup is updated
|
|
53
|
+
# won't need this once lamindb is released with the new pin
|
|
54
|
+
kwargs["run"] = None # can't yet access tracking information
|
|
55
|
+
kwargs["in_db"] = False
|
|
56
|
+
for db_field, base_col in RENAME.items():
|
|
57
|
+
kwargs[db_field] = kwargs.pop(base_col)
|
|
58
|
+
if kwargs["entity"] in bionty_models:
|
|
59
|
+
kwargs["entity"] = f"bionty.{kwargs['entity']}"
|
|
60
|
+
record = Source(**kwargs)
|
|
61
|
+
all_records.append(record)
|
|
62
|
+
|
|
63
|
+
Source.objects.bulk_create(all_records, ignore_conflicts=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_bionty_sources(isettings: InstanceSettings):
|
|
67
|
+
"""Write currently_used bionty sources to LAMINDB_VERSIONS_PATH in bionty."""
|
|
68
|
+
if "bionty" not in isettings.schema:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
import bionty.base as bionty_base
|
|
72
|
+
from bionty.base.dev._handle_sources import parse_currently_used_sources
|
|
73
|
+
from bionty.base.dev._io import write_yaml
|
|
74
|
+
from bionty.models import Source
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# need try except because of integer primary key migration
|
|
78
|
+
active_records = (
|
|
79
|
+
Source.objects.filter(currently_used=True).order_by("id").all().values()
|
|
80
|
+
)
|
|
81
|
+
for kwargs in active_records:
|
|
82
|
+
for db_field, base_col in RENAME.items():
|
|
83
|
+
kwargs[base_col] = kwargs.pop(db_field)
|
|
84
|
+
# TODO: non-bionty schema?
|
|
85
|
+
kwargs["entity"] = kwargs["entity"].replace("bionty.", "")
|
|
86
|
+
write_yaml(
|
|
87
|
+
parse_currently_used_sources(active_records),
|
|
88
|
+
bionty_base.settings.lamindb_sources,
|
|
89
|
+
)
|
|
90
|
+
except (OperationalError, ProgrammingError):
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def delete_bionty_sources_yaml():
|
|
95
|
+
"""Delete LAMINDB_SOURCES_PATH in bionty."""
|
|
96
|
+
try:
|
|
97
|
+
import bionty.base as bionty_base
|
|
98
|
+
|
|
99
|
+
bionty_base.settings.lamindb_sources.unlink(missing_ok=True)
|
|
100
|
+
except ModuleNotFoundError:
|
|
101
|
+
pass
|