lamindb_setup 1.8.3__py3-none-any.whl → 1.9.1__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 +107 -107
- lamindb_setup/_cache.py +87 -87
- lamindb_setup/_check_setup.py +166 -166
- lamindb_setup/_connect_instance.py +328 -342
- lamindb_setup/_delete.py +141 -141
- lamindb_setup/_disconnect.py +32 -32
- lamindb_setup/_init_instance.py +440 -440
- lamindb_setup/_migrate.py +266 -259
- lamindb_setup/_register_instance.py +35 -35
- lamindb_setup/_schema_metadata.py +441 -441
- lamindb_setup/_set_managed_storage.py +70 -70
- lamindb_setup/_setup_user.py +133 -133
- lamindb_setup/core/__init__.py +21 -21
- lamindb_setup/core/_aws_options.py +223 -211
- lamindb_setup/core/_hub_client.py +248 -243
- lamindb_setup/core/_hub_core.py +665 -663
- lamindb_setup/core/_hub_crud.py +227 -227
- lamindb_setup/core/_private_django_api.py +83 -83
- lamindb_setup/core/_settings.py +377 -364
- lamindb_setup/core/_settings_instance.py +569 -568
- lamindb_setup/core/_settings_load.py +141 -141
- lamindb_setup/core/_settings_save.py +95 -95
- lamindb_setup/core/_settings_storage.py +429 -429
- lamindb_setup/core/_settings_store.py +91 -91
- lamindb_setup/core/_settings_user.py +55 -55
- lamindb_setup/core/_setup_bionty_sources.py +44 -44
- lamindb_setup/core/cloud_sqlite_locker.py +240 -240
- lamindb_setup/core/django.py +305 -291
- lamindb_setup/core/exceptions.py +1 -1
- lamindb_setup/core/hashing.py +134 -134
- lamindb_setup/core/types.py +1 -1
- lamindb_setup/core/upath.py +1013 -1009
- lamindb_setup/errors.py +70 -70
- lamindb_setup/types.py +20 -20
- {lamindb_setup-1.8.3.dist-info → lamindb_setup-1.9.1.dist-info}/METADATA +1 -1
- lamindb_setup-1.9.1.dist-info/RECORD +50 -0
- lamindb_setup-1.8.3.dist-info/RECORD +0 -50
- {lamindb_setup-1.8.3.dist-info → lamindb_setup-1.9.1.dist-info}/LICENSE +0 -0
- {lamindb_setup-1.8.3.dist-info → lamindb_setup-1.9.1.dist-info}/WHEEL +0 -0
lamindb_setup/core/_hub_core.py
CHANGED
|
@@ -1,663 +1,665 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import uuid
|
|
6
|
-
from importlib import metadata
|
|
7
|
-
from typing import TYPE_CHECKING, Literal
|
|
8
|
-
from uuid import UUID
|
|
9
|
-
|
|
10
|
-
import jwt
|
|
11
|
-
from lamin_utils import logger
|
|
12
|
-
from postgrest.exceptions import APIError
|
|
13
|
-
|
|
14
|
-
from lamindb_setup._migrate import check_whether_migrations_in_sync
|
|
15
|
-
|
|
16
|
-
from ._hub_client import (
|
|
17
|
-
call_with_fallback,
|
|
18
|
-
call_with_fallback_auth,
|
|
19
|
-
connect_hub,
|
|
20
|
-
request_with_auth,
|
|
21
|
-
)
|
|
22
|
-
from ._hub_crud import (
|
|
23
|
-
_delete_instance_record,
|
|
24
|
-
select_account_by_handle,
|
|
25
|
-
select_db_user_by_instance,
|
|
26
|
-
select_default_storage_by_instance_id,
|
|
27
|
-
select_instance_by_id_with_storage,
|
|
28
|
-
select_instance_by_name,
|
|
29
|
-
select_instance_by_owner_name,
|
|
30
|
-
)
|
|
31
|
-
from ._hub_crud import update_instance as _update_instance_record
|
|
32
|
-
from ._hub_utils import (
|
|
33
|
-
LaminDsn,
|
|
34
|
-
LaminDsnModel,
|
|
35
|
-
)
|
|
36
|
-
from ._settings import settings
|
|
37
|
-
from ._settings_instance import InstanceSettings
|
|
38
|
-
from ._settings_storage import StorageSettings, base62, instance_uid_from_uuid
|
|
39
|
-
from .hashing import hash_and_encode_as_b62
|
|
40
|
-
|
|
41
|
-
if TYPE_CHECKING:
|
|
42
|
-
from supabase import Client # type: ignore
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def delete_storage_record(
|
|
46
|
-
storage_info: dict[str, str] | StorageSettings,
|
|
47
|
-
access_token: str | None = None,
|
|
48
|
-
) -> None:
|
|
49
|
-
if isinstance(storage_info, StorageSettings):
|
|
50
|
-
storage_info = {"id": storage_info._uuid.hex, "root": storage_info.root_as_str} # type: ignore
|
|
51
|
-
return call_with_fallback_auth(
|
|
52
|
-
_delete_storage_record, storage_info=storage_info, access_token=access_token
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def _delete_storage_record(storage_info: dict[str, str], client: Client) -> None:
|
|
57
|
-
storage_uuid = UUID(storage_info["id"])
|
|
58
|
-
if storage_uuid is None:
|
|
59
|
-
return None
|
|
60
|
-
response = client.table("storage").delete().eq("id", storage_uuid.hex).execute()
|
|
61
|
-
if response.data:
|
|
62
|
-
logger.important(
|
|
63
|
-
f"deleted storage record on hub {storage_uuid.hex} | {storage_info['root']}"
|
|
64
|
-
)
|
|
65
|
-
else:
|
|
66
|
-
raise PermissionError(
|
|
67
|
-
f"Deleting of storage {storage_uuid.hex} ({storage_info['root']}) was not successful. Probably, you"
|
|
68
|
-
" don't have sufficient permissions."
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def update_instance_record(instance_uuid: UUID, fields: dict) -> None:
|
|
73
|
-
return call_with_fallback_auth(
|
|
74
|
-
_update_instance_record, instance_id=instance_uuid.hex, instance_fields=fields
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def get_storage_records_for_instance(
|
|
79
|
-
instance_id: UUID,
|
|
80
|
-
) -> list[dict[str, str | int]]:
|
|
81
|
-
return call_with_fallback_auth(
|
|
82
|
-
_get_storage_records_for_instance,
|
|
83
|
-
instance_id=instance_id,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _get_storage_records_for_instance(
|
|
88
|
-
instance_id: UUID, client: Client
|
|
89
|
-
) -> list[dict[str, str | int]]:
|
|
90
|
-
response = (
|
|
91
|
-
client.table("storage").select("*").eq("instance_id", instance_id.hex).execute()
|
|
92
|
-
)
|
|
93
|
-
return response.data
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def _select_storage(
|
|
97
|
-
ssettings: StorageSettings, update_uid: bool, client: Client
|
|
98
|
-
) -> bool:
|
|
99
|
-
root = ssettings.root_as_str
|
|
100
|
-
response = client.table("storage").select("*").eq("root", root).execute()
|
|
101
|
-
if not response.data:
|
|
102
|
-
return False
|
|
103
|
-
else:
|
|
104
|
-
existing_storage = response.data[0]
|
|
105
|
-
if existing_storage["instance_id"] is None:
|
|
106
|
-
# if there is no instance_id, the storage location should not be on the hub
|
|
107
|
-
# this can only occur if instance init fails halfway through and is not cleaned up
|
|
108
|
-
# we're patching the situation here
|
|
109
|
-
existing_storage["instance_id"] = (
|
|
110
|
-
ssettings._instance_id.hex
|
|
111
|
-
if ssettings._instance_id is not None
|
|
112
|
-
else None
|
|
113
|
-
)
|
|
114
|
-
if ssettings._instance_id is not None:
|
|
115
|
-
if UUID(existing_storage["instance_id"]) != ssettings._instance_id:
|
|
116
|
-
logger.debug(
|
|
117
|
-
f"referencing storage location {root}, which is managed by instance {existing_storage['instance_id']}"
|
|
118
|
-
)
|
|
119
|
-
ssettings._instance_id = UUID(existing_storage["instance_id"])
|
|
120
|
-
ssettings._uuid_ = UUID(existing_storage["id"])
|
|
121
|
-
if update_uid:
|
|
122
|
-
ssettings._uid = existing_storage["lnid"]
|
|
123
|
-
else:
|
|
124
|
-
assert ssettings._uid == existing_storage["lnid"]
|
|
125
|
-
return True
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _select_storage_or_parent(path: str, client: Client) -> dict | None:
|
|
129
|
-
result = client.rpc("existing_root_or_child", {"_path": path}).execute().data
|
|
130
|
-
if result["root"] is None:
|
|
131
|
-
return None
|
|
132
|
-
result["uid"] = result.pop("lnid")
|
|
133
|
-
result["instance_uid"] = instance_uid_from_uuid(UUID(result.pop("instance_id")))
|
|
134
|
-
return result
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def select_storage_or_parent(path: str, access_token: str | None = None) -> dict | None:
|
|
138
|
-
if settings.user.handle != "anonymous" or access_token is not None:
|
|
139
|
-
return call_with_fallback_auth(
|
|
140
|
-
_select_storage_or_parent,
|
|
141
|
-
path=path,
|
|
142
|
-
access_token=access_token,
|
|
143
|
-
)
|
|
144
|
-
else:
|
|
145
|
-
return call_with_fallback(_select_storage_or_parent, path=path)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def init_storage_hub(
|
|
149
|
-
ssettings: StorageSettings,
|
|
150
|
-
auto_populate_instance: bool = True,
|
|
151
|
-
created_by: UUID | None = None,
|
|
152
|
-
access_token: str | None = None,
|
|
153
|
-
prevent_creation: bool = False,
|
|
154
|
-
) -> Literal["hub-record-retrieved", "hub-record-created", "hub-record-not-created"]:
|
|
155
|
-
"""Creates or retrieves an existing storage record from the hub."""
|
|
156
|
-
if settings.user.handle != "anonymous" or access_token is not None:
|
|
157
|
-
return call_with_fallback_auth(
|
|
158
|
-
_init_storage_hub,
|
|
159
|
-
ssettings=ssettings,
|
|
160
|
-
auto_populate_instance=auto_populate_instance,
|
|
161
|
-
created_by=created_by,
|
|
162
|
-
access_token=access_token,
|
|
163
|
-
prevent_creation=prevent_creation,
|
|
164
|
-
)
|
|
165
|
-
else:
|
|
166
|
-
storage_exists = call_with_fallback(
|
|
167
|
-
_select_storage, ssettings=ssettings, update_uid=True
|
|
168
|
-
)
|
|
169
|
-
if storage_exists:
|
|
170
|
-
return "hub-record-retrieved"
|
|
171
|
-
else:
|
|
172
|
-
return "hub-record-not-created"
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def _init_storage_hub(
|
|
176
|
-
client: Client,
|
|
177
|
-
ssettings: StorageSettings,
|
|
178
|
-
auto_populate_instance: bool,
|
|
179
|
-
created_by: UUID | None = None,
|
|
180
|
-
prevent_creation: bool = False,
|
|
181
|
-
) -> Literal["hub-record-retrieved", "hub-record-created", "hub-record-not-created"]:
|
|
182
|
-
from lamindb_setup import settings
|
|
183
|
-
|
|
184
|
-
created_by = settings.user._uuid if created_by is None else created_by
|
|
185
|
-
# storage roots are always stored without the trailing slash in the SQL
|
|
186
|
-
# database
|
|
187
|
-
root = ssettings.root_as_str
|
|
188
|
-
if _select_storage(ssettings, update_uid=True, client=client):
|
|
189
|
-
return "hub-record-retrieved"
|
|
190
|
-
if prevent_creation:
|
|
191
|
-
return "hub-record-not-created"
|
|
192
|
-
if ssettings.type_is_cloud:
|
|
193
|
-
id = uuid.uuid5(uuid.NAMESPACE_URL, root)
|
|
194
|
-
else:
|
|
195
|
-
id = uuid.uuid4()
|
|
196
|
-
if (
|
|
197
|
-
ssettings._instance_id is None
|
|
198
|
-
and settings._instance_exists
|
|
199
|
-
and auto_populate_instance
|
|
200
|
-
):
|
|
201
|
-
logger.warning(
|
|
202
|
-
f"will manage storage location {ssettings.root_as_str} with instance {settings.instance.slug}"
|
|
203
|
-
)
|
|
204
|
-
ssettings._instance_id = settings.instance._id
|
|
205
|
-
instance_id_hex = (
|
|
206
|
-
ssettings._instance_id.hex
|
|
207
|
-
if (ssettings._instance_id is not None and auto_populate_instance)
|
|
208
|
-
else None
|
|
209
|
-
)
|
|
210
|
-
fields = {
|
|
211
|
-
"id": id.hex,
|
|
212
|
-
"lnid": ssettings.uid,
|
|
213
|
-
"created_by": created_by.hex, # type: ignore
|
|
214
|
-
"root": root,
|
|
215
|
-
"region": ssettings.region,
|
|
216
|
-
"type": ssettings.type,
|
|
217
|
-
"instance_id": instance_id_hex,
|
|
218
|
-
# the empty string is important as we want the user flow to be through LaminHub
|
|
219
|
-
# if this errors with unique constraint error, the user has to update
|
|
220
|
-
# the description in LaminHub
|
|
221
|
-
"description": "",
|
|
222
|
-
}
|
|
223
|
-
# TODO: add error message for violated unique constraint
|
|
224
|
-
# on root & description
|
|
225
|
-
client.table("storage").upsert(fields).execute()
|
|
226
|
-
ssettings._uuid_ = id
|
|
227
|
-
return "hub-record-created"
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def delete_instance(identifier: UUID | str, require_empty: bool = True) -> str | None:
|
|
231
|
-
return call_with_fallback_auth(
|
|
232
|
-
_delete_instance, identifier=identifier, require_empty=require_empty
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
def _delete_instance(
|
|
237
|
-
identifier: UUID | str, require_empty: bool, client: Client
|
|
238
|
-
) -> str | None:
|
|
239
|
-
"""Fully delete an instance in the hub.
|
|
240
|
-
|
|
241
|
-
This function deletes the relevant instance and storage records in the hub,
|
|
242
|
-
conditional on the emptiness of the storage location.
|
|
243
|
-
"""
|
|
244
|
-
from ._settings_storage import mark_storage_root
|
|
245
|
-
from .upath import check_storage_is_empty, create_path
|
|
246
|
-
|
|
247
|
-
# the "/" check is for backward compatibility with the old identifier format
|
|
248
|
-
if isinstance(identifier, UUID) or "/" not in identifier:
|
|
249
|
-
if isinstance(identifier, UUID):
|
|
250
|
-
instance_id_str = identifier.hex
|
|
251
|
-
else:
|
|
252
|
-
instance_id_str = identifier
|
|
253
|
-
instance_with_storage = select_instance_by_id_with_storage(
|
|
254
|
-
instance_id=instance_id_str, client=client
|
|
255
|
-
)
|
|
256
|
-
else:
|
|
257
|
-
owner, name = identifier.split("/")
|
|
258
|
-
instance_with_storage = select_instance_by_owner_name(
|
|
259
|
-
owner=owner, name=name, client=client
|
|
260
|
-
)
|
|
261
|
-
|
|
262
|
-
if instance_with_storage is None:
|
|
263
|
-
logger.important("not deleting instance from hub as instance not found there")
|
|
264
|
-
return "instance-not-found"
|
|
265
|
-
|
|
266
|
-
storage_records = _get_storage_records_for_instance(
|
|
267
|
-
UUID(instance_with_storage["id"]),
|
|
268
|
-
client,
|
|
269
|
-
)
|
|
270
|
-
if require_empty:
|
|
271
|
-
for storage_record in storage_records:
|
|
272
|
-
root_string: str = storage_record["root"] # type: ignore
|
|
273
|
-
account_for_sqlite_file = (
|
|
274
|
-
instance_with_storage["db_scheme"] is None
|
|
275
|
-
and instance_with_storage["storage"]["root"] == root_string
|
|
276
|
-
)
|
|
277
|
-
# gate storage and instance deletion on empty storage location for
|
|
278
|
-
# normally auth.get_session() doesn't have access_token
|
|
279
|
-
# so this block is useless i think (Sergei)
|
|
280
|
-
# the token is received from user settings inside create_path
|
|
281
|
-
# might be needed in the hub though
|
|
282
|
-
if client.auth.get_session() is not None:
|
|
283
|
-
access_token = client.auth.get_session().access_token
|
|
284
|
-
else:
|
|
285
|
-
access_token = None
|
|
286
|
-
root_path = create_path(root_string, access_token)
|
|
287
|
-
check_storage_is_empty(
|
|
288
|
-
root_path, account_for_sqlite_file=account_for_sqlite_file
|
|
289
|
-
)
|
|
290
|
-
for storage_record in storage_records:
|
|
291
|
-
_delete_storage_record(storage_record, client) # type: ignore
|
|
292
|
-
_delete_instance_record(UUID(instance_with_storage["id"]), client)
|
|
293
|
-
return None
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def delete_instance_record(instance_id: UUID, access_token: str | None = None) -> None:
|
|
297
|
-
return call_with_fallback_auth(
|
|
298
|
-
_delete_instance_record, instance_id=instance_id, access_token=access_token
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def init_instance_hub(
|
|
303
|
-
isettings: InstanceSettings,
|
|
304
|
-
account_id: UUID | None = None,
|
|
305
|
-
access_token: str | None = None,
|
|
306
|
-
) -> None:
|
|
307
|
-
return call_with_fallback_auth(
|
|
308
|
-
_init_instance_hub,
|
|
309
|
-
isettings=isettings,
|
|
310
|
-
account_id=account_id,
|
|
311
|
-
access_token=access_token,
|
|
312
|
-
)
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
def _init_instance_hub(
|
|
316
|
-
client: Client, isettings: InstanceSettings, account_id: UUID | None = None
|
|
317
|
-
) -> None:
|
|
318
|
-
from ._settings import settings
|
|
319
|
-
|
|
320
|
-
account_id = settings.user._uuid if account_id is None else account_id
|
|
321
|
-
|
|
322
|
-
try:
|
|
323
|
-
lamindb_version = metadata.version("lamindb")
|
|
324
|
-
except metadata.PackageNotFoundError:
|
|
325
|
-
lamindb_version = None
|
|
326
|
-
fields = {
|
|
327
|
-
"id": isettings._id.hex,
|
|
328
|
-
"account_id": account_id.hex, # type: ignore
|
|
329
|
-
"name": isettings.name,
|
|
330
|
-
"lnid": isettings.uid,
|
|
331
|
-
"schema_str": isettings._schema_str,
|
|
332
|
-
"lamindb_version": lamindb_version,
|
|
333
|
-
"public": False,
|
|
334
|
-
}
|
|
335
|
-
if isettings.dialect != "sqlite":
|
|
336
|
-
db_dsn = LaminDsnModel(db=isettings.db)
|
|
337
|
-
db_fields = {
|
|
338
|
-
"db_scheme": db_dsn.db.scheme,
|
|
339
|
-
"db_host": db_dsn.db.host,
|
|
340
|
-
"db_port": db_dsn.db.port,
|
|
341
|
-
"db_database": db_dsn.db.database,
|
|
342
|
-
}
|
|
343
|
-
fields.update(db_fields)
|
|
344
|
-
slug = isettings.slug
|
|
345
|
-
# I'd like the following to be an upsert, but this seems to violate RLS
|
|
346
|
-
# Similarly, if we don't specify `returning="minimal"`, we'll violate RLS
|
|
347
|
-
# we could make this idempotent by catching an error, but this seems dangerous
|
|
348
|
-
# as then init_instance is no longer idempotent
|
|
349
|
-
try:
|
|
350
|
-
client.table("instance").insert(fields, returning="minimal").execute()
|
|
351
|
-
except APIError:
|
|
352
|
-
logger.warning(f"instance already existed at: https://lamin.ai/{slug}")
|
|
353
|
-
return None
|
|
354
|
-
client.table("storage").update(
|
|
355
|
-
{"instance_id": isettings._id.hex, "is_default": True}
|
|
356
|
-
).eq("id", isettings.storage._uuid.hex).execute() # type: ignore
|
|
357
|
-
if isettings.dialect != "sqlite" and isettings.is_remote:
|
|
358
|
-
logger.important(f"go to: https://lamin.ai/{slug}")
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def _connect_instance_hub(
|
|
362
|
-
owner: str, # account_handle
|
|
363
|
-
name: str, # instance_name
|
|
364
|
-
client: Client,
|
|
365
|
-
) -> tuple[dict, dict] | str:
|
|
366
|
-
response = client.functions.invoke(
|
|
367
|
-
"get-instance-settings-v1",
|
|
368
|
-
invoke_options={"body": {"owner": owner, "name": name}},
|
|
369
|
-
)
|
|
370
|
-
# check instance renames
|
|
371
|
-
if response == b"{}":
|
|
372
|
-
data = (
|
|
373
|
-
client.table("instance_previous_name")
|
|
374
|
-
.select(
|
|
375
|
-
"instance!instance_previous_name_instance_id_17ac5d61_fk_instance_id(name, account!instance_account_id_28936e8f_fk_account_id(handle))"
|
|
376
|
-
)
|
|
377
|
-
.eq("instance.account.handle", owner)
|
|
378
|
-
.eq("previous_name", name)
|
|
379
|
-
.execute()
|
|
380
|
-
.data
|
|
381
|
-
)
|
|
382
|
-
if len(data) != 0 and (instance_data := data[0]["instance"]) is not None:
|
|
383
|
-
new_name = instance_data["name"]
|
|
384
|
-
# the instance was renamed
|
|
385
|
-
if new_name != name:
|
|
386
|
-
logger.warning(
|
|
387
|
-
f"'{owner}/{name}' was renamed, please use '{owner}/{new_name}'"
|
|
388
|
-
)
|
|
389
|
-
response = client.functions.invoke(
|
|
390
|
-
"get-instance-settings-v1",
|
|
391
|
-
invoke_options={"body": {"owner": owner, "name": new_name}},
|
|
392
|
-
)
|
|
393
|
-
# no instance found, check why is that
|
|
394
|
-
if response == b"{}":
|
|
395
|
-
# try the via single requests, will take more time
|
|
396
|
-
account = select_account_by_handle(owner, client)
|
|
397
|
-
if account is None:
|
|
398
|
-
return "account-not-exists"
|
|
399
|
-
instance = select_instance_by_name(account["id"], name, client)
|
|
400
|
-
if instance is None:
|
|
401
|
-
return "instance-not-found"
|
|
402
|
-
# get default storage
|
|
403
|
-
storage = select_default_storage_by_instance_id(instance["id"], client)
|
|
404
|
-
if storage is None:
|
|
405
|
-
return "default-storage-does-not-exist-on-hub"
|
|
406
|
-
logger.warning(
|
|
407
|
-
"could not find instance via API, but found directly querying hub"
|
|
408
|
-
)
|
|
409
|
-
else:
|
|
410
|
-
instance = json.loads(response)
|
|
411
|
-
storage = instance.pop("storage")
|
|
412
|
-
|
|
413
|
-
if instance["db_scheme"] is not None:
|
|
414
|
-
db_user_name, db_user_password = None, None
|
|
415
|
-
if "db_user_name" in instance and "db_user_password" in instance:
|
|
416
|
-
db_user_name, db_user_password = (
|
|
417
|
-
instance["db_user_name"],
|
|
418
|
-
instance["db_user_password"],
|
|
419
|
-
)
|
|
420
|
-
else:
|
|
421
|
-
db_user = select_db_user_by_instance(instance["id"], client)
|
|
422
|
-
if db_user is not None:
|
|
423
|
-
db_user_name, db_user_password = (
|
|
424
|
-
db_user["db_user_name"],
|
|
425
|
-
db_user["db_user_password"],
|
|
426
|
-
)
|
|
427
|
-
db_dsn = LaminDsn.build(
|
|
428
|
-
scheme=instance["db_scheme"],
|
|
429
|
-
user=db_user_name if db_user_name is not None else "none",
|
|
430
|
-
password=db_user_password if db_user_password is not None else "none",
|
|
431
|
-
host=instance["db_host"],
|
|
432
|
-
port=instance["db_port"],
|
|
433
|
-
database=instance["db_database"],
|
|
434
|
-
)
|
|
435
|
-
instance["db"] = db_dsn
|
|
436
|
-
check_whether_migrations_in_sync(instance["lamindb_version"])
|
|
437
|
-
return instance, storage # type: ignore
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
def connect_instance_hub(
|
|
441
|
-
*,
|
|
442
|
-
owner: str, # account_handle
|
|
443
|
-
name: str, # instance_name
|
|
444
|
-
access_token: str | None = None,
|
|
445
|
-
) -> tuple[dict, dict] | str:
|
|
446
|
-
from ._settings import settings
|
|
447
|
-
|
|
448
|
-
if settings.user.handle != "anonymous" or access_token is not None:
|
|
449
|
-
return call_with_fallback_auth(
|
|
450
|
-
_connect_instance_hub, owner=owner, name=name, access_token=access_token
|
|
451
|
-
)
|
|
452
|
-
else:
|
|
453
|
-
return call_with_fallback(_connect_instance_hub, owner=owner, name=name)
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
def access_aws(storage_root: str, access_token: str | None = None) -> dict[str, dict]:
|
|
457
|
-
from ._settings import settings
|
|
458
|
-
|
|
459
|
-
if settings.user.handle != "anonymous" or access_token is not None:
|
|
460
|
-
storage_root_info = call_with_fallback_auth(
|
|
461
|
-
_access_aws, storage_root=storage_root, access_token=access_token
|
|
462
|
-
)
|
|
463
|
-
else:
|
|
464
|
-
storage_root_info = call_with_fallback(_access_aws, storage_root=storage_root)
|
|
465
|
-
return storage_root_info
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
def _access_aws(*, storage_root: str, client: Client) -> dict[str, dict]:
|
|
469
|
-
import lamindb_setup
|
|
470
|
-
|
|
471
|
-
storage_root_info: dict[str, dict] = {"credentials": {}, "accessibility": {}}
|
|
472
|
-
response = client.functions.invoke(
|
|
473
|
-
"get-cloud-access-v1",
|
|
474
|
-
invoke_options={"body": {"storage_root": storage_root}},
|
|
475
|
-
)
|
|
476
|
-
if response is not None and response != b"{}":
|
|
477
|
-
data = json.loads(response)
|
|
478
|
-
|
|
479
|
-
loaded_credentials = data["Credentials"]
|
|
480
|
-
loaded_accessibility = data["StorageAccessibility"]
|
|
481
|
-
|
|
482
|
-
credentials = storage_root_info["credentials"]
|
|
483
|
-
credentials["key"] = loaded_credentials["AccessKeyId"]
|
|
484
|
-
credentials["secret"] = loaded_credentials["SecretAccessKey"]
|
|
485
|
-
credentials["token"] = loaded_credentials["SessionToken"]
|
|
486
|
-
|
|
487
|
-
accessibility = storage_root_info["accessibility"]
|
|
488
|
-
accessibility["storage_root"] = loaded_accessibility["storageRoot"]
|
|
489
|
-
accessibility["is_managed"] = loaded_accessibility["isManaged"]
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
return "
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
client
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import uuid
|
|
6
|
+
from importlib import metadata
|
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
import jwt
|
|
11
|
+
from lamin_utils import logger
|
|
12
|
+
from postgrest.exceptions import APIError
|
|
13
|
+
|
|
14
|
+
from lamindb_setup._migrate import check_whether_migrations_in_sync
|
|
15
|
+
|
|
16
|
+
from ._hub_client import (
|
|
17
|
+
call_with_fallback,
|
|
18
|
+
call_with_fallback_auth,
|
|
19
|
+
connect_hub,
|
|
20
|
+
request_with_auth,
|
|
21
|
+
)
|
|
22
|
+
from ._hub_crud import (
|
|
23
|
+
_delete_instance_record,
|
|
24
|
+
select_account_by_handle,
|
|
25
|
+
select_db_user_by_instance,
|
|
26
|
+
select_default_storage_by_instance_id,
|
|
27
|
+
select_instance_by_id_with_storage,
|
|
28
|
+
select_instance_by_name,
|
|
29
|
+
select_instance_by_owner_name,
|
|
30
|
+
)
|
|
31
|
+
from ._hub_crud import update_instance as _update_instance_record
|
|
32
|
+
from ._hub_utils import (
|
|
33
|
+
LaminDsn,
|
|
34
|
+
LaminDsnModel,
|
|
35
|
+
)
|
|
36
|
+
from ._settings import settings
|
|
37
|
+
from ._settings_instance import InstanceSettings
|
|
38
|
+
from ._settings_storage import StorageSettings, base62, instance_uid_from_uuid
|
|
39
|
+
from .hashing import hash_and_encode_as_b62
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from supabase import Client # type: ignore
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def delete_storage_record(
|
|
46
|
+
storage_info: dict[str, str] | StorageSettings,
|
|
47
|
+
access_token: str | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
if isinstance(storage_info, StorageSettings):
|
|
50
|
+
storage_info = {"id": storage_info._uuid.hex, "root": storage_info.root_as_str} # type: ignore
|
|
51
|
+
return call_with_fallback_auth(
|
|
52
|
+
_delete_storage_record, storage_info=storage_info, access_token=access_token
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _delete_storage_record(storage_info: dict[str, str], client: Client) -> None:
|
|
57
|
+
storage_uuid = UUID(storage_info["id"])
|
|
58
|
+
if storage_uuid is None:
|
|
59
|
+
return None
|
|
60
|
+
response = client.table("storage").delete().eq("id", storage_uuid.hex).execute()
|
|
61
|
+
if response.data:
|
|
62
|
+
logger.important(
|
|
63
|
+
f"deleted storage record on hub {storage_uuid.hex} | {storage_info['root']}"
|
|
64
|
+
)
|
|
65
|
+
else:
|
|
66
|
+
raise PermissionError(
|
|
67
|
+
f"Deleting of storage {storage_uuid.hex} ({storage_info['root']}) was not successful. Probably, you"
|
|
68
|
+
" don't have sufficient permissions."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def update_instance_record(instance_uuid: UUID, fields: dict) -> None:
|
|
73
|
+
return call_with_fallback_auth(
|
|
74
|
+
_update_instance_record, instance_id=instance_uuid.hex, instance_fields=fields
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_storage_records_for_instance(
|
|
79
|
+
instance_id: UUID,
|
|
80
|
+
) -> list[dict[str, str | int]]:
|
|
81
|
+
return call_with_fallback_auth(
|
|
82
|
+
_get_storage_records_for_instance,
|
|
83
|
+
instance_id=instance_id,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _get_storage_records_for_instance(
|
|
88
|
+
instance_id: UUID, client: Client
|
|
89
|
+
) -> list[dict[str, str | int]]:
|
|
90
|
+
response = (
|
|
91
|
+
client.table("storage").select("*").eq("instance_id", instance_id.hex).execute()
|
|
92
|
+
)
|
|
93
|
+
return response.data
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _select_storage(
|
|
97
|
+
ssettings: StorageSettings, update_uid: bool, client: Client
|
|
98
|
+
) -> bool:
|
|
99
|
+
root = ssettings.root_as_str
|
|
100
|
+
response = client.table("storage").select("*").eq("root", root).execute()
|
|
101
|
+
if not response.data:
|
|
102
|
+
return False
|
|
103
|
+
else:
|
|
104
|
+
existing_storage = response.data[0]
|
|
105
|
+
if existing_storage["instance_id"] is None:
|
|
106
|
+
# if there is no instance_id, the storage location should not be on the hub
|
|
107
|
+
# this can only occur if instance init fails halfway through and is not cleaned up
|
|
108
|
+
# we're patching the situation here
|
|
109
|
+
existing_storage["instance_id"] = (
|
|
110
|
+
ssettings._instance_id.hex
|
|
111
|
+
if ssettings._instance_id is not None
|
|
112
|
+
else None
|
|
113
|
+
)
|
|
114
|
+
if ssettings._instance_id is not None:
|
|
115
|
+
if UUID(existing_storage["instance_id"]) != ssettings._instance_id:
|
|
116
|
+
logger.debug(
|
|
117
|
+
f"referencing storage location {root}, which is managed by instance {existing_storage['instance_id']}"
|
|
118
|
+
)
|
|
119
|
+
ssettings._instance_id = UUID(existing_storage["instance_id"])
|
|
120
|
+
ssettings._uuid_ = UUID(existing_storage["id"])
|
|
121
|
+
if update_uid:
|
|
122
|
+
ssettings._uid = existing_storage["lnid"]
|
|
123
|
+
else:
|
|
124
|
+
assert ssettings._uid == existing_storage["lnid"]
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _select_storage_or_parent(path: str, client: Client) -> dict | None:
|
|
129
|
+
result = client.rpc("existing_root_or_child", {"_path": path}).execute().data
|
|
130
|
+
if result["root"] is None:
|
|
131
|
+
return None
|
|
132
|
+
result["uid"] = result.pop("lnid")
|
|
133
|
+
result["instance_uid"] = instance_uid_from_uuid(UUID(result.pop("instance_id")))
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def select_storage_or_parent(path: str, access_token: str | None = None) -> dict | None:
|
|
138
|
+
if settings.user.handle != "anonymous" or access_token is not None:
|
|
139
|
+
return call_with_fallback_auth(
|
|
140
|
+
_select_storage_or_parent,
|
|
141
|
+
path=path,
|
|
142
|
+
access_token=access_token,
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
return call_with_fallback(_select_storage_or_parent, path=path)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def init_storage_hub(
|
|
149
|
+
ssettings: StorageSettings,
|
|
150
|
+
auto_populate_instance: bool = True,
|
|
151
|
+
created_by: UUID | None = None,
|
|
152
|
+
access_token: str | None = None,
|
|
153
|
+
prevent_creation: bool = False,
|
|
154
|
+
) -> Literal["hub-record-retrieved", "hub-record-created", "hub-record-not-created"]:
|
|
155
|
+
"""Creates or retrieves an existing storage record from the hub."""
|
|
156
|
+
if settings.user.handle != "anonymous" or access_token is not None:
|
|
157
|
+
return call_with_fallback_auth(
|
|
158
|
+
_init_storage_hub,
|
|
159
|
+
ssettings=ssettings,
|
|
160
|
+
auto_populate_instance=auto_populate_instance,
|
|
161
|
+
created_by=created_by,
|
|
162
|
+
access_token=access_token,
|
|
163
|
+
prevent_creation=prevent_creation,
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
storage_exists = call_with_fallback(
|
|
167
|
+
_select_storage, ssettings=ssettings, update_uid=True
|
|
168
|
+
)
|
|
169
|
+
if storage_exists:
|
|
170
|
+
return "hub-record-retrieved"
|
|
171
|
+
else:
|
|
172
|
+
return "hub-record-not-created"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _init_storage_hub(
|
|
176
|
+
client: Client,
|
|
177
|
+
ssettings: StorageSettings,
|
|
178
|
+
auto_populate_instance: bool,
|
|
179
|
+
created_by: UUID | None = None,
|
|
180
|
+
prevent_creation: bool = False,
|
|
181
|
+
) -> Literal["hub-record-retrieved", "hub-record-created", "hub-record-not-created"]:
|
|
182
|
+
from lamindb_setup import settings
|
|
183
|
+
|
|
184
|
+
created_by = settings.user._uuid if created_by is None else created_by
|
|
185
|
+
# storage roots are always stored without the trailing slash in the SQL
|
|
186
|
+
# database
|
|
187
|
+
root = ssettings.root_as_str
|
|
188
|
+
if _select_storage(ssettings, update_uid=True, client=client):
|
|
189
|
+
return "hub-record-retrieved"
|
|
190
|
+
if prevent_creation:
|
|
191
|
+
return "hub-record-not-created"
|
|
192
|
+
if ssettings.type_is_cloud:
|
|
193
|
+
id = uuid.uuid5(uuid.NAMESPACE_URL, root)
|
|
194
|
+
else:
|
|
195
|
+
id = uuid.uuid4()
|
|
196
|
+
if (
|
|
197
|
+
ssettings._instance_id is None
|
|
198
|
+
and settings._instance_exists
|
|
199
|
+
and auto_populate_instance
|
|
200
|
+
):
|
|
201
|
+
logger.warning(
|
|
202
|
+
f"will manage storage location {ssettings.root_as_str} with instance {settings.instance.slug}"
|
|
203
|
+
)
|
|
204
|
+
ssettings._instance_id = settings.instance._id
|
|
205
|
+
instance_id_hex = (
|
|
206
|
+
ssettings._instance_id.hex
|
|
207
|
+
if (ssettings._instance_id is not None and auto_populate_instance)
|
|
208
|
+
else None
|
|
209
|
+
)
|
|
210
|
+
fields = {
|
|
211
|
+
"id": id.hex,
|
|
212
|
+
"lnid": ssettings.uid,
|
|
213
|
+
"created_by": created_by.hex, # type: ignore
|
|
214
|
+
"root": root,
|
|
215
|
+
"region": ssettings.region,
|
|
216
|
+
"type": ssettings.type,
|
|
217
|
+
"instance_id": instance_id_hex,
|
|
218
|
+
# the empty string is important as we want the user flow to be through LaminHub
|
|
219
|
+
# if this errors with unique constraint error, the user has to update
|
|
220
|
+
# the description in LaminHub
|
|
221
|
+
"description": "",
|
|
222
|
+
}
|
|
223
|
+
# TODO: add error message for violated unique constraint
|
|
224
|
+
# on root & description
|
|
225
|
+
client.table("storage").upsert(fields).execute()
|
|
226
|
+
ssettings._uuid_ = id
|
|
227
|
+
return "hub-record-created"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def delete_instance(identifier: UUID | str, require_empty: bool = True) -> str | None:
|
|
231
|
+
return call_with_fallback_auth(
|
|
232
|
+
_delete_instance, identifier=identifier, require_empty=require_empty
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _delete_instance(
|
|
237
|
+
identifier: UUID | str, require_empty: bool, client: Client
|
|
238
|
+
) -> str | None:
|
|
239
|
+
"""Fully delete an instance in the hub.
|
|
240
|
+
|
|
241
|
+
This function deletes the relevant instance and storage records in the hub,
|
|
242
|
+
conditional on the emptiness of the storage location.
|
|
243
|
+
"""
|
|
244
|
+
from ._settings_storage import mark_storage_root
|
|
245
|
+
from .upath import check_storage_is_empty, create_path
|
|
246
|
+
|
|
247
|
+
# the "/" check is for backward compatibility with the old identifier format
|
|
248
|
+
if isinstance(identifier, UUID) or "/" not in identifier:
|
|
249
|
+
if isinstance(identifier, UUID):
|
|
250
|
+
instance_id_str = identifier.hex
|
|
251
|
+
else:
|
|
252
|
+
instance_id_str = identifier
|
|
253
|
+
instance_with_storage = select_instance_by_id_with_storage(
|
|
254
|
+
instance_id=instance_id_str, client=client
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
owner, name = identifier.split("/")
|
|
258
|
+
instance_with_storage = select_instance_by_owner_name(
|
|
259
|
+
owner=owner, name=name, client=client
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if instance_with_storage is None:
|
|
263
|
+
logger.important("not deleting instance from hub as instance not found there")
|
|
264
|
+
return "instance-not-found"
|
|
265
|
+
|
|
266
|
+
storage_records = _get_storage_records_for_instance(
|
|
267
|
+
UUID(instance_with_storage["id"]),
|
|
268
|
+
client,
|
|
269
|
+
)
|
|
270
|
+
if require_empty:
|
|
271
|
+
for storage_record in storage_records:
|
|
272
|
+
root_string: str = storage_record["root"] # type: ignore
|
|
273
|
+
account_for_sqlite_file = (
|
|
274
|
+
instance_with_storage["db_scheme"] is None
|
|
275
|
+
and instance_with_storage["storage"]["root"] == root_string
|
|
276
|
+
)
|
|
277
|
+
# gate storage and instance deletion on empty storage location for
|
|
278
|
+
# normally auth.get_session() doesn't have access_token
|
|
279
|
+
# so this block is useless i think (Sergei)
|
|
280
|
+
# the token is received from user settings inside create_path
|
|
281
|
+
# might be needed in the hub though
|
|
282
|
+
if client.auth.get_session() is not None:
|
|
283
|
+
access_token = client.auth.get_session().access_token
|
|
284
|
+
else:
|
|
285
|
+
access_token = None
|
|
286
|
+
root_path = create_path(root_string, access_token)
|
|
287
|
+
check_storage_is_empty(
|
|
288
|
+
root_path, account_for_sqlite_file=account_for_sqlite_file
|
|
289
|
+
)
|
|
290
|
+
for storage_record in storage_records:
|
|
291
|
+
_delete_storage_record(storage_record, client) # type: ignore
|
|
292
|
+
_delete_instance_record(UUID(instance_with_storage["id"]), client)
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def delete_instance_record(instance_id: UUID, access_token: str | None = None) -> None:
|
|
297
|
+
return call_with_fallback_auth(
|
|
298
|
+
_delete_instance_record, instance_id=instance_id, access_token=access_token
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def init_instance_hub(
|
|
303
|
+
isettings: InstanceSettings,
|
|
304
|
+
account_id: UUID | None = None,
|
|
305
|
+
access_token: str | None = None,
|
|
306
|
+
) -> None:
|
|
307
|
+
return call_with_fallback_auth(
|
|
308
|
+
_init_instance_hub,
|
|
309
|
+
isettings=isettings,
|
|
310
|
+
account_id=account_id,
|
|
311
|
+
access_token=access_token,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _init_instance_hub(
|
|
316
|
+
client: Client, isettings: InstanceSettings, account_id: UUID | None = None
|
|
317
|
+
) -> None:
|
|
318
|
+
from ._settings import settings
|
|
319
|
+
|
|
320
|
+
account_id = settings.user._uuid if account_id is None else account_id
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
lamindb_version = metadata.version("lamindb")
|
|
324
|
+
except metadata.PackageNotFoundError:
|
|
325
|
+
lamindb_version = None
|
|
326
|
+
fields = {
|
|
327
|
+
"id": isettings._id.hex,
|
|
328
|
+
"account_id": account_id.hex, # type: ignore
|
|
329
|
+
"name": isettings.name,
|
|
330
|
+
"lnid": isettings.uid,
|
|
331
|
+
"schema_str": isettings._schema_str,
|
|
332
|
+
"lamindb_version": lamindb_version,
|
|
333
|
+
"public": False,
|
|
334
|
+
}
|
|
335
|
+
if isettings.dialect != "sqlite":
|
|
336
|
+
db_dsn = LaminDsnModel(db=isettings.db)
|
|
337
|
+
db_fields = {
|
|
338
|
+
"db_scheme": db_dsn.db.scheme,
|
|
339
|
+
"db_host": db_dsn.db.host,
|
|
340
|
+
"db_port": db_dsn.db.port,
|
|
341
|
+
"db_database": db_dsn.db.database,
|
|
342
|
+
}
|
|
343
|
+
fields.update(db_fields)
|
|
344
|
+
slug = isettings.slug
|
|
345
|
+
# I'd like the following to be an upsert, but this seems to violate RLS
|
|
346
|
+
# Similarly, if we don't specify `returning="minimal"`, we'll violate RLS
|
|
347
|
+
# we could make this idempotent by catching an error, but this seems dangerous
|
|
348
|
+
# as then init_instance is no longer idempotent
|
|
349
|
+
try:
|
|
350
|
+
client.table("instance").insert(fields, returning="minimal").execute()
|
|
351
|
+
except APIError:
|
|
352
|
+
logger.warning(f"instance already existed at: https://lamin.ai/{slug}")
|
|
353
|
+
return None
|
|
354
|
+
client.table("storage").update(
|
|
355
|
+
{"instance_id": isettings._id.hex, "is_default": True}
|
|
356
|
+
).eq("id", isettings.storage._uuid.hex).execute() # type: ignore
|
|
357
|
+
if isettings.dialect != "sqlite" and isettings.is_remote:
|
|
358
|
+
logger.important(f"go to: https://lamin.ai/{slug}")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _connect_instance_hub(
|
|
362
|
+
owner: str, # account_handle
|
|
363
|
+
name: str, # instance_name
|
|
364
|
+
client: Client,
|
|
365
|
+
) -> tuple[dict, dict] | str:
|
|
366
|
+
response = client.functions.invoke(
|
|
367
|
+
"get-instance-settings-v1",
|
|
368
|
+
invoke_options={"body": {"owner": owner, "name": name}},
|
|
369
|
+
)
|
|
370
|
+
# check instance renames
|
|
371
|
+
if response == b"{}":
|
|
372
|
+
data = (
|
|
373
|
+
client.table("instance_previous_name")
|
|
374
|
+
.select(
|
|
375
|
+
"instance!instance_previous_name_instance_id_17ac5d61_fk_instance_id(name, account!instance_account_id_28936e8f_fk_account_id(handle))"
|
|
376
|
+
)
|
|
377
|
+
.eq("instance.account.handle", owner)
|
|
378
|
+
.eq("previous_name", name)
|
|
379
|
+
.execute()
|
|
380
|
+
.data
|
|
381
|
+
)
|
|
382
|
+
if len(data) != 0 and (instance_data := data[0]["instance"]) is not None:
|
|
383
|
+
new_name = instance_data["name"]
|
|
384
|
+
# the instance was renamed
|
|
385
|
+
if new_name != name:
|
|
386
|
+
logger.warning(
|
|
387
|
+
f"'{owner}/{name}' was renamed, please use '{owner}/{new_name}'"
|
|
388
|
+
)
|
|
389
|
+
response = client.functions.invoke(
|
|
390
|
+
"get-instance-settings-v1",
|
|
391
|
+
invoke_options={"body": {"owner": owner, "name": new_name}},
|
|
392
|
+
)
|
|
393
|
+
# no instance found, check why is that
|
|
394
|
+
if response == b"{}":
|
|
395
|
+
# try the via single requests, will take more time
|
|
396
|
+
account = select_account_by_handle(owner, client)
|
|
397
|
+
if account is None:
|
|
398
|
+
return "account-not-exists"
|
|
399
|
+
instance = select_instance_by_name(account["id"], name, client)
|
|
400
|
+
if instance is None:
|
|
401
|
+
return "instance-not-found"
|
|
402
|
+
# get default storage
|
|
403
|
+
storage = select_default_storage_by_instance_id(instance["id"], client)
|
|
404
|
+
if storage is None:
|
|
405
|
+
return "default-storage-does-not-exist-on-hub"
|
|
406
|
+
logger.warning(
|
|
407
|
+
"could not find instance via API, but found directly querying hub"
|
|
408
|
+
)
|
|
409
|
+
else:
|
|
410
|
+
instance = json.loads(response)
|
|
411
|
+
storage = instance.pop("storage")
|
|
412
|
+
|
|
413
|
+
if instance["db_scheme"] is not None:
|
|
414
|
+
db_user_name, db_user_password = None, None
|
|
415
|
+
if "db_user_name" in instance and "db_user_password" in instance:
|
|
416
|
+
db_user_name, db_user_password = (
|
|
417
|
+
instance["db_user_name"],
|
|
418
|
+
instance["db_user_password"],
|
|
419
|
+
)
|
|
420
|
+
else:
|
|
421
|
+
db_user = select_db_user_by_instance(instance["id"], client)
|
|
422
|
+
if db_user is not None:
|
|
423
|
+
db_user_name, db_user_password = (
|
|
424
|
+
db_user["db_user_name"],
|
|
425
|
+
db_user["db_user_password"],
|
|
426
|
+
)
|
|
427
|
+
db_dsn = LaminDsn.build(
|
|
428
|
+
scheme=instance["db_scheme"],
|
|
429
|
+
user=db_user_name if db_user_name is not None else "none",
|
|
430
|
+
password=db_user_password if db_user_password is not None else "none",
|
|
431
|
+
host=instance["db_host"],
|
|
432
|
+
port=instance["db_port"],
|
|
433
|
+
database=instance["db_database"],
|
|
434
|
+
)
|
|
435
|
+
instance["db"] = db_dsn
|
|
436
|
+
check_whether_migrations_in_sync(instance["lamindb_version"])
|
|
437
|
+
return instance, storage # type: ignore
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def connect_instance_hub(
|
|
441
|
+
*,
|
|
442
|
+
owner: str, # account_handle
|
|
443
|
+
name: str, # instance_name
|
|
444
|
+
access_token: str | None = None,
|
|
445
|
+
) -> tuple[dict, dict] | str:
|
|
446
|
+
from ._settings import settings
|
|
447
|
+
|
|
448
|
+
if settings.user.handle != "anonymous" or access_token is not None:
|
|
449
|
+
return call_with_fallback_auth(
|
|
450
|
+
_connect_instance_hub, owner=owner, name=name, access_token=access_token
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
return call_with_fallback(_connect_instance_hub, owner=owner, name=name)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def access_aws(storage_root: str, access_token: str | None = None) -> dict[str, dict]:
|
|
457
|
+
from ._settings import settings
|
|
458
|
+
|
|
459
|
+
if settings.user.handle != "anonymous" or access_token is not None:
|
|
460
|
+
storage_root_info = call_with_fallback_auth(
|
|
461
|
+
_access_aws, storage_root=storage_root, access_token=access_token
|
|
462
|
+
)
|
|
463
|
+
else:
|
|
464
|
+
storage_root_info = call_with_fallback(_access_aws, storage_root=storage_root)
|
|
465
|
+
return storage_root_info
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _access_aws(*, storage_root: str, client: Client) -> dict[str, dict]:
|
|
469
|
+
import lamindb_setup
|
|
470
|
+
|
|
471
|
+
storage_root_info: dict[str, dict] = {"credentials": {}, "accessibility": {}}
|
|
472
|
+
response = client.functions.invoke(
|
|
473
|
+
"get-cloud-access-v1",
|
|
474
|
+
invoke_options={"body": {"storage_root": storage_root}},
|
|
475
|
+
)
|
|
476
|
+
if response is not None and response != b"{}":
|
|
477
|
+
data = json.loads(response)
|
|
478
|
+
|
|
479
|
+
loaded_credentials = data["Credentials"]
|
|
480
|
+
loaded_accessibility = data["StorageAccessibility"]
|
|
481
|
+
|
|
482
|
+
credentials = storage_root_info["credentials"]
|
|
483
|
+
credentials["key"] = loaded_credentials["AccessKeyId"]
|
|
484
|
+
credentials["secret"] = loaded_credentials["SecretAccessKey"]
|
|
485
|
+
credentials["token"] = loaded_credentials["SessionToken"]
|
|
486
|
+
|
|
487
|
+
accessibility = storage_root_info["accessibility"]
|
|
488
|
+
accessibility["storage_root"] = loaded_accessibility["storageRoot"]
|
|
489
|
+
accessibility["is_managed"] = loaded_accessibility["isManaged"]
|
|
490
|
+
accessibility["extra_parameters"] = loaded_accessibility.get("extraParameters")
|
|
491
|
+
|
|
492
|
+
return storage_root_info
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def access_db(
|
|
496
|
+
instance: InstanceSettings | dict, access_token: str | None = None
|
|
497
|
+
) -> str:
|
|
498
|
+
instance_id: UUID
|
|
499
|
+
instance_slug: str
|
|
500
|
+
instance_api_url: str | None
|
|
501
|
+
if (
|
|
502
|
+
"LAMIN_DB_TOKEN" in os.environ
|
|
503
|
+
and (env_db_token := os.environ["LAMIN_DB_TOKEN"]) != ""
|
|
504
|
+
):
|
|
505
|
+
return env_db_token
|
|
506
|
+
|
|
507
|
+
if isinstance(instance, InstanceSettings):
|
|
508
|
+
instance_id = instance._id
|
|
509
|
+
instance_slug = instance.slug
|
|
510
|
+
instance_api_url = instance._api_url
|
|
511
|
+
else:
|
|
512
|
+
instance_id = UUID(instance["id"])
|
|
513
|
+
instance_slug = instance["owner"] + "/" + instance["name"]
|
|
514
|
+
instance_api_url = instance["api_url"]
|
|
515
|
+
|
|
516
|
+
if access_token is None:
|
|
517
|
+
if settings.user.handle == "anonymous":
|
|
518
|
+
raise RuntimeError(
|
|
519
|
+
f"Can only get fine-grained access to {instance_slug} if authenticated."
|
|
520
|
+
)
|
|
521
|
+
else:
|
|
522
|
+
access_token = settings.user.access_token
|
|
523
|
+
renew_token = True
|
|
524
|
+
else:
|
|
525
|
+
renew_token = False
|
|
526
|
+
# local is used in tests
|
|
527
|
+
url = f"/access_v2/instances/{instance_id}/db_token"
|
|
528
|
+
if os.environ.get("LAMIN_ENV", "prod") != "local":
|
|
529
|
+
if instance_api_url is None:
|
|
530
|
+
raise RuntimeError(
|
|
531
|
+
f"Can only get fine-grained access to {instance_slug} if api_url is present."
|
|
532
|
+
)
|
|
533
|
+
url = instance_api_url + url
|
|
534
|
+
|
|
535
|
+
response = request_with_auth(url, "get", access_token, renew_token) # type: ignore
|
|
536
|
+
status_code = response.status_code
|
|
537
|
+
if not (200 <= status_code < 300):
|
|
538
|
+
raise PermissionError(
|
|
539
|
+
f"Fine-grained access to {instance_slug} failed: {status_code} {response.text}"
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
response_json = response.json()
|
|
543
|
+
if "token" not in response_json:
|
|
544
|
+
raise RuntimeError("The response of access_db does not contain a db token.")
|
|
545
|
+
return response_json["token"]
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def get_lamin_site_base_url():
|
|
549
|
+
if "LAMIN_ENV" in os.environ:
|
|
550
|
+
if os.environ["LAMIN_ENV"] == "local":
|
|
551
|
+
return "http://localhost:3000"
|
|
552
|
+
elif os.environ["LAMIN_ENV"] == "staging":
|
|
553
|
+
return "https://staging.lamin.ai"
|
|
554
|
+
return "https://lamin.ai"
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def sign_up_local_hub(email) -> str | tuple[str, str, str]:
|
|
558
|
+
# raises gotrue.errors.AuthApiError: User already registered
|
|
559
|
+
password = base62(40) # generate new password
|
|
560
|
+
sign_up_kwargs = {"email": email, "password": password}
|
|
561
|
+
client = connect_hub()
|
|
562
|
+
auth_response = client.auth.sign_up(sign_up_kwargs)
|
|
563
|
+
client.auth.sign_out()
|
|
564
|
+
return (
|
|
565
|
+
password,
|
|
566
|
+
auth_response.session.user.id,
|
|
567
|
+
auth_response.session.access_token,
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _sign_in_hub(email: str, password: str, handle: str | None, client: Client):
|
|
572
|
+
auth = client.auth.sign_in_with_password(
|
|
573
|
+
{
|
|
574
|
+
"email": email,
|
|
575
|
+
"password": password,
|
|
576
|
+
}
|
|
577
|
+
)
|
|
578
|
+
data = client.table("account").select("*").eq("id", auth.user.id).execute().data
|
|
579
|
+
if data: # sync data from hub to local cache in case it was updated on the hub
|
|
580
|
+
user = data[0]
|
|
581
|
+
user_uuid = UUID(user["id"])
|
|
582
|
+
user_id = user["lnid"]
|
|
583
|
+
user_handle = user["handle"]
|
|
584
|
+
user_name = user["name"]
|
|
585
|
+
if handle is not None and handle != user_handle:
|
|
586
|
+
logger.warning(
|
|
587
|
+
f"using account handle {user_handle} (cached handle was {handle})"
|
|
588
|
+
)
|
|
589
|
+
else: # user did not complete signup as usermeta has no matching row
|
|
590
|
+
logger.error("complete signup on your account page.")
|
|
591
|
+
return "complete-signup"
|
|
592
|
+
return (
|
|
593
|
+
user_uuid,
|
|
594
|
+
user_id,
|
|
595
|
+
user_handle,
|
|
596
|
+
user_name,
|
|
597
|
+
auth.session.access_token,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def sign_in_hub(
|
|
602
|
+
email: str, password: str, handle: str | None = None
|
|
603
|
+
) -> Exception | str | tuple[UUID, str, str, str, str]:
|
|
604
|
+
try:
|
|
605
|
+
result = call_with_fallback(
|
|
606
|
+
_sign_in_hub, email=email, password=password, handle=handle
|
|
607
|
+
)
|
|
608
|
+
except Exception as exception: # this is bad, but I don't find APIError right now
|
|
609
|
+
logger.error(exception)
|
|
610
|
+
logger.error(
|
|
611
|
+
"Could not login. Probably your password is wrong or you didn't complete"
|
|
612
|
+
" signup."
|
|
613
|
+
)
|
|
614
|
+
return exception
|
|
615
|
+
return result
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _sign_in_hub_api_key(api_key: str, client: Client):
|
|
619
|
+
response = client.functions.invoke(
|
|
620
|
+
"get-jwt-v1",
|
|
621
|
+
invoke_options={"body": {"api_key": api_key}},
|
|
622
|
+
)
|
|
623
|
+
access_token = json.loads(response)["accessToken"]
|
|
624
|
+
# probably need more info here to avoid additional queries
|
|
625
|
+
# like handle, uid etc
|
|
626
|
+
account_id = jwt.decode(access_token, options={"verify_signature": False})["sub"]
|
|
627
|
+
client.postgrest.auth(access_token)
|
|
628
|
+
# normally public.account.id is equal to auth.user.id
|
|
629
|
+
data = client.table("account").select("*").eq("id", account_id).execute().data
|
|
630
|
+
if data:
|
|
631
|
+
user = data[0]
|
|
632
|
+
user_uuid = UUID(user["id"])
|
|
633
|
+
user_id = user["lnid"]
|
|
634
|
+
user_handle = user["handle"]
|
|
635
|
+
user_name = user["name"]
|
|
636
|
+
else:
|
|
637
|
+
logger.error("Invalid API key.")
|
|
638
|
+
return "invalid-api-key"
|
|
639
|
+
return (user_uuid, user_id, user_handle, user_name, access_token)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def sign_in_hub_api_key(
|
|
643
|
+
api_key: str,
|
|
644
|
+
) -> Exception | str | tuple[UUID, str, str, str, str]:
|
|
645
|
+
try:
|
|
646
|
+
result = call_with_fallback(_sign_in_hub_api_key, api_key=api_key)
|
|
647
|
+
except Exception as exception:
|
|
648
|
+
logger.error(exception)
|
|
649
|
+
logger.error("Could not login. Probably your API key is wrong.")
|
|
650
|
+
return exception
|
|
651
|
+
return result
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _create_api_key(body: dict, client: Client) -> str:
|
|
655
|
+
response = client.functions.invoke(
|
|
656
|
+
"create-api-key-v1",
|
|
657
|
+
invoke_options={"body": body},
|
|
658
|
+
)
|
|
659
|
+
api_key = json.loads(response)["apiKey"]
|
|
660
|
+
return api_key
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def create_api_key(body: dict) -> str:
|
|
664
|
+
api_key = call_with_fallback_auth(_create_api_key, body=body)
|
|
665
|
+
return api_key
|