lamindb_setup 0.77.3__py2.py3-none-any.whl → 0.77.5__py2.py3-none-any.whl

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