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