lamindb_setup 0.78.0__py3-none-any.whl

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