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,485 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Literal
7
+
8
+ from django.db.utils import ProgrammingError
9
+ from lamin_utils import logger
10
+
11
+ from ._hub_client import call_with_fallback
12
+ from ._hub_crud import select_account_handle_name_by_lnid
13
+ from ._hub_utils import LaminDsn, LaminDsnModel
14
+ from ._settings_save import save_instance_settings
15
+ from ._settings_storage import StorageSettings, init_storage, mark_storage_root
16
+ from ._settings_store import current_instance_settings_file, instance_settings_file
17
+ from .cloud_sqlite_locker import (
18
+ EXPIRATION_TIME,
19
+ InstanceLockedException,
20
+ )
21
+ from .upath import LocalPathClasses, UPath
22
+
23
+ if TYPE_CHECKING:
24
+ from uuid import UUID
25
+
26
+ from ._settings_user import UserSettings
27
+
28
+
29
+ def sanitize_git_repo_url(repo_url: str) -> str:
30
+ assert repo_url.startswith("https://")
31
+ return repo_url.replace(".git", "")
32
+
33
+
34
+ def is_local_db_url(db_url: str) -> bool:
35
+ if "@localhost:" in db_url:
36
+ return True
37
+ if "@0.0.0.0:" in db_url:
38
+ return True
39
+ if "@127.0.0.1" in db_url:
40
+ return True
41
+ return False
42
+
43
+
44
+ class InstanceSettings:
45
+ """Instance settings."""
46
+
47
+ def __init__(
48
+ self,
49
+ id: UUID, # instance id/uuid
50
+ owner: str, # owner handle
51
+ name: str, # instance name
52
+ storage: StorageSettings, # storage location
53
+ keep_artifacts_local: bool = False, # default to local storage
54
+ uid: str | None = None, # instance uid/lnid
55
+ db: str | None = None, # DB URI
56
+ schema: str | None = None, # comma-separated string of schema names
57
+ git_repo: str | None = None, # a git repo URL
58
+ is_on_hub: bool | None = None, # initialized from hub
59
+ api_url: str | None = None,
60
+ schema_id: UUID | None = None,
61
+ _locker_user: UserSettings | None = None, # user to lock for if cloud sqlite
62
+ ):
63
+ from ._hub_utils import validate_db_arg
64
+
65
+ self._id_: UUID = id
66
+ self._owner: str = owner
67
+ self._name: str = name
68
+ self._uid: str | None = uid
69
+ self._storage: StorageSettings = storage
70
+ validate_db_arg(db)
71
+ self._db: str | None = db
72
+ self._schema_str: str | None = schema
73
+ self._git_repo = None if git_repo is None else sanitize_git_repo_url(git_repo)
74
+ # local storage
75
+ self._keep_artifacts_local = keep_artifacts_local
76
+ self._storage_local: StorageSettings | None = None
77
+ self._is_on_hub = is_on_hub
78
+ # private, needed for writing instance settings
79
+ self._api_url = api_url
80
+ self._schema_id = schema_id
81
+ # if None then settings.user is used
82
+ self._locker_user = _locker_user
83
+
84
+ def __repr__(self):
85
+ """Rich string representation."""
86
+ representation = f"Current instance: {self.slug}"
87
+ attrs = ["owner", "name", "storage", "db", "schema", "git_repo"]
88
+ for attr in attrs:
89
+ value = getattr(self, attr)
90
+ if attr == "storage":
91
+ representation += f"\n- storage root: {value.root_as_str}"
92
+ representation += f"\n- storage region: {value.region}"
93
+ elif attr == "db":
94
+ if self.dialect != "sqlite":
95
+ model = LaminDsnModel(db=value)
96
+ db_print = LaminDsn.build(
97
+ scheme=model.db.scheme,
98
+ user=model.db.user,
99
+ password="***",
100
+ host="***",
101
+ port=model.db.port,
102
+ database=model.db.database,
103
+ )
104
+ else:
105
+ db_print = value
106
+ representation += f"\n- {attr}: {db_print}"
107
+ else:
108
+ representation += f"\n- {attr}: {value}"
109
+ return representation
110
+
111
+ @property
112
+ def owner(self) -> str:
113
+ """Instance owner. A user or organization account handle."""
114
+ return self._owner
115
+
116
+ @property
117
+ def name(self) -> str:
118
+ """Instance name."""
119
+ return self._name
120
+
121
+ def _search_local_root(
122
+ self, local_root: str | None = None, mute_warning: bool = False
123
+ ) -> StorageSettings | None:
124
+ from lnschema_core.models import Storage
125
+
126
+ if local_root is not None:
127
+ local_records = Storage.objects.filter(root=local_root)
128
+ else:
129
+ # only search local managed storage locations (instance_uid=self.uid)
130
+ local_records = Storage.objects.filter(type="local", instance_uid=self.uid)
131
+ all_local_records = local_records.all()
132
+ try:
133
+ # trigger an error in case of a migration issue
134
+ all_local_records.first()
135
+ except ProgrammingError:
136
+ logger.error("not able to load Storage registry: please migrate")
137
+ return None
138
+ found = False
139
+ for record in all_local_records:
140
+ root_path = Path(record.root)
141
+ if root_path.exists():
142
+ marker_path = root_path / ".lamindb/_is_initialized"
143
+ if marker_path.exists():
144
+ try:
145
+ uid = marker_path.read_text()
146
+ except PermissionError:
147
+ logger.warning(
148
+ f"ignoring the following location because no permission to read it: {marker_path}"
149
+ )
150
+ continue
151
+ if uid == record.uid:
152
+ found = True
153
+ break
154
+ elif uid == "":
155
+ try:
156
+ # legacy instance that was not yet marked properly
157
+ mark_storage_root(record.root, record.uid)
158
+ except PermissionError:
159
+ logger.warning(
160
+ f"ignoring the following location because no permission to write to it: {marker_path}"
161
+ )
162
+ continue
163
+ else:
164
+ continue
165
+ else:
166
+ # legacy instance that was not yet marked at all
167
+ mark_storage_root(record.root, record.uid)
168
+ break
169
+ if found:
170
+ return StorageSettings(record.root)
171
+ elif not mute_warning:
172
+ logger.warning(
173
+ f"none of the registered local storage locations were found in your environment: {local_records}"
174
+ )
175
+ logger.important(
176
+ "please register a new local storage location via `ln.settings.storage_local = local_root_path` and re-load/connect the instance"
177
+ )
178
+ return None
179
+
180
+ @property
181
+ def keep_artifacts_local(self) -> bool:
182
+ """Default to keeping artifacts local.
183
+
184
+ Enable this optional setting for cloud instances on lamin.ai.
185
+
186
+ Guide: :doc:`faq/keep-artifacts-local`
187
+ """
188
+ return self._keep_artifacts_local
189
+
190
+ @property
191
+ def storage(self) -> StorageSettings:
192
+ """Default storage.
193
+
194
+ For a cloud instance, this is cloud storage. For a local instance, this
195
+ is a local directory.
196
+ """
197
+ return self._storage
198
+
199
+ @property
200
+ def storage_local(self) -> StorageSettings:
201
+ """An additional local default storage.
202
+
203
+ Is only available if :attr:`keep_artifacts_local` is enabled.
204
+
205
+ Guide: :doc:`faq/keep-artifacts-local`
206
+ """
207
+ if not self._keep_artifacts_local:
208
+ raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
209
+ if self._storage_local is None:
210
+ self._storage_local = self._search_local_root()
211
+ if self._storage_local is None:
212
+ # raise an error, there was a warning just before in search_local_root
213
+ raise ValueError()
214
+ return self._storage_local
215
+
216
+ @storage_local.setter
217
+ def storage_local(self, local_root: Path | str):
218
+ from lamindb_setup._init_instance import register_storage_in_instance
219
+
220
+ local_root = Path(local_root)
221
+ if not self._keep_artifacts_local:
222
+ raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
223
+ storage_local = self._search_local_root(
224
+ local_root=StorageSettings(local_root).root_as_str, mute_warning=True
225
+ )
226
+ if storage_local is not None:
227
+ # great, we're merely switching storage location
228
+ self._storage_local = storage_local
229
+ logger.important(f"defaulting to local storage: {storage_local.root}")
230
+ return None
231
+ storage_local = self._search_local_root(mute_warning=True)
232
+ if storage_local is not None:
233
+ if os.getenv("LAMIN_TESTING") == "true":
234
+ response = "y"
235
+ else:
236
+ response = input(
237
+ "You already configured a local storage root for this instance in this"
238
+ f" environment: {self.storage_local.root}\nDo you want to register another one? (y/n)"
239
+ )
240
+ if response != "y":
241
+ return None
242
+ local_root = UPath(local_root)
243
+ assert isinstance(local_root, LocalPathClasses)
244
+ self._storage_local, _ = init_storage(local_root, self._id, register_hub=True) # type: ignore
245
+ register_storage_in_instance(self._storage_local) # type: ignore
246
+ logger.important(f"defaulting to local storage: {self._storage_local.root}")
247
+
248
+ @property
249
+ def slug(self) -> str:
250
+ """Unique semantic identifier of form `"{account_handle}/{instance_name}"`."""
251
+ return f"{self.owner}/{self.name}"
252
+
253
+ @property
254
+ def git_repo(self) -> str | None:
255
+ """Sync transforms with scripts in git repository.
256
+
257
+ Provide the full git repo URL.
258
+ """
259
+ return self._git_repo
260
+
261
+ @property
262
+ def _id(self) -> UUID:
263
+ """The internal instance id."""
264
+ return self._id_
265
+
266
+ @property
267
+ def uid(self) -> str:
268
+ """The user-facing instance id."""
269
+ from .hashing import hash_and_encode_as_b62
270
+
271
+ return hash_and_encode_as_b62(self._id.hex)[:12]
272
+
273
+ @property
274
+ def schema(self) -> set[str]:
275
+ """Schema modules in addition to core schema."""
276
+ if self._schema_str is None:
277
+ return {} # type: ignore
278
+ else:
279
+ return {schema for schema in self._schema_str.split(",") if schema != ""}
280
+
281
+ @property
282
+ def _sqlite_file(self) -> UPath:
283
+ """SQLite file."""
284
+ return self.storage.key_to_filepath(f"{self._id.hex}.lndb")
285
+
286
+ @property
287
+ def _sqlite_file_local(self) -> Path:
288
+ """Local SQLite file."""
289
+ return self.storage.cloud_to_local_no_update(self._sqlite_file)
290
+
291
+ def _update_cloud_sqlite_file(self, unlock_cloud_sqlite: bool = True) -> None:
292
+ """Upload the local sqlite file to the cloud file."""
293
+ if self._is_cloud_sqlite:
294
+ sqlite_file = self._sqlite_file
295
+ logger.warning(
296
+ f"updating{' & unlocking' if unlock_cloud_sqlite else ''} cloud SQLite "
297
+ f"'{sqlite_file}' of instance"
298
+ f" '{self.slug}'"
299
+ )
300
+ cache_file = self.storage.cloud_to_local_no_update(sqlite_file)
301
+ sqlite_file.upload_from(cache_file, print_progress=True) # type: ignore
302
+ cloud_mtime = sqlite_file.modified.timestamp() # type: ignore
303
+ # this seems to work even if there is an open connection
304
+ # to the cache file
305
+ os.utime(cache_file, times=(cloud_mtime, cloud_mtime))
306
+ if unlock_cloud_sqlite:
307
+ self._cloud_sqlite_locker.unlock()
308
+
309
+ def _update_local_sqlite_file(self, lock_cloud_sqlite: bool = True) -> None:
310
+ """Download the cloud sqlite file if it is newer than local."""
311
+ if self._is_cloud_sqlite:
312
+ logger.warning(
313
+ "updating local SQLite & locking cloud SQLite (sync back & unlock:"
314
+ " lamin disconnect)"
315
+ )
316
+ if lock_cloud_sqlite:
317
+ self._cloud_sqlite_locker.lock()
318
+ self._check_sqlite_lock()
319
+ sqlite_file = self._sqlite_file
320
+ cache_file = self.storage.cloud_to_local_no_update(sqlite_file)
321
+ sqlite_file.synchronize(cache_file, print_progress=True) # type: ignore
322
+
323
+ def _check_sqlite_lock(self):
324
+ if not self._cloud_sqlite_locker.has_lock:
325
+ locked_by = self._cloud_sqlite_locker._locked_by
326
+ lock_msg = "Cannot load the instance, it is locked by "
327
+ user_info = call_with_fallback(
328
+ select_account_handle_name_by_lnid,
329
+ lnid=locked_by,
330
+ )
331
+ if user_info is None:
332
+ lock_msg += f"uid: '{locked_by}'."
333
+ else:
334
+ lock_msg += (
335
+ f"'{user_info['handle']}' (uid: '{locked_by}', name:"
336
+ f" '{user_info['name']}')."
337
+ )
338
+ lock_msg += (
339
+ " The instance will be automatically unlocked after"
340
+ f" {int(EXPIRATION_TIME/3600/24)}d of no activity."
341
+ )
342
+ raise InstanceLockedException(lock_msg)
343
+
344
+ @property
345
+ def db(self) -> str:
346
+ """Database connection string (URI)."""
347
+ if "LAMINDB_DJANGO_DATABASE_URL" in os.environ:
348
+ logger.warning(
349
+ "LAMINDB_DJANGO_DATABASE_URL env variable "
350
+ f"is set to {os.environ['LAMINDB_DJANGO_DATABASE_URL']}. "
351
+ "It overwrites all db connections and is used instead of `instance.db`."
352
+ )
353
+ if self._db is None:
354
+ # here, we want the updated sqlite file
355
+ # hence, we don't use self._sqlite_file_local()
356
+ # error_no_origin=False because on instance init
357
+ # the sqlite file is not yet in the cloud
358
+ sqlite_filepath = self.storage.cloud_to_local(
359
+ self._sqlite_file, error_no_origin=False
360
+ )
361
+ return f"sqlite:///{sqlite_filepath}"
362
+ else:
363
+ return self._db
364
+
365
+ @property
366
+ def dialect(self) -> Literal["sqlite", "postgresql"]:
367
+ """SQL dialect."""
368
+ if self._db is None or self._db.startswith("sqlite://"):
369
+ return "sqlite"
370
+ else:
371
+ assert self._db.startswith("postgresql"), f"Unexpected DB value: {self._db}"
372
+ return "postgresql"
373
+
374
+ @property
375
+ def _is_cloud_sqlite(self) -> bool:
376
+ # can we make this a private property, Sergei?
377
+ # as it's not relevant to the user
378
+ """Is this a cloud instance with sqlite db."""
379
+ return self.dialect == "sqlite" and self.storage.type_is_cloud
380
+
381
+ @property
382
+ def _cloud_sqlite_locker(self):
383
+ # avoid circular import
384
+ from .cloud_sqlite_locker import empty_locker, get_locker
385
+
386
+ if self._is_cloud_sqlite:
387
+ try:
388
+ # if _locker_user is None then settings.user is used
389
+ return get_locker(self, self._locker_user)
390
+ except PermissionError:
391
+ logger.warning("read-only access - did not access locker")
392
+ return empty_locker
393
+ else:
394
+ return empty_locker
395
+
396
+ @property
397
+ def is_remote(self) -> bool:
398
+ """Boolean indicating if an instance has no local component."""
399
+ if not self.storage.type_is_cloud:
400
+ return False
401
+
402
+ if self.dialect == "postgresql":
403
+ if is_local_db_url(self.db):
404
+ return False
405
+ # returns True for cloud SQLite
406
+ # and remote postgres
407
+ return True
408
+
409
+ @property
410
+ def is_on_hub(self) -> bool:
411
+ """Is this instance on the hub?
412
+
413
+ Can only reliably establish if user has access to the instance. Will
414
+ return `False` in case the instance isn't found.
415
+ """
416
+ if self._is_on_hub is None:
417
+ from ._hub_client import call_with_fallback_auth
418
+ from ._hub_crud import select_instance_by_id
419
+ from ._settings import settings
420
+
421
+ if settings.user.handle != "anonymous":
422
+ response = call_with_fallback_auth(
423
+ select_instance_by_id, instance_id=self._id.hex
424
+ )
425
+ else:
426
+ response = call_with_fallback(
427
+ select_instance_by_id, instance_id=self._id.hex
428
+ )
429
+ logger.warning("calling anonymously, will miss private instances")
430
+ if response is None:
431
+ self._is_on_hub = False
432
+ else:
433
+ self._is_on_hub = True
434
+ return self._is_on_hub
435
+
436
+ def _get_settings_file(self) -> Path:
437
+ return instance_settings_file(self.name, self.owner)
438
+
439
+ def _persist(self, write_to_disk: bool = True) -> None:
440
+ """Set these instance settings as the current instance.
441
+
442
+ Args:
443
+ write_to_disk: Save these instance settings to disk and
444
+ overwrite the current instance settings file.
445
+ """
446
+ if write_to_disk:
447
+ assert self.name is not None
448
+ filepath = self._get_settings_file()
449
+ # persist under filepath for later reference
450
+ save_instance_settings(self, filepath)
451
+ # persist under current file for auto load
452
+ shutil.copy2(filepath, current_instance_settings_file())
453
+ # persist under settings class for same session reference
454
+ # need to import here to avoid circular import
455
+ from ._settings import settings
456
+
457
+ settings._instance_settings = self
458
+
459
+ def _init_db(self):
460
+ from .django import setup_django
461
+
462
+ setup_django(self, init=True)
463
+
464
+ def _load_db(self) -> tuple[bool, str]:
465
+ # Is the database available and initialized as LaminDB?
466
+ # returns a tuple of status code and message
467
+ if self.dialect == "sqlite" and not self._sqlite_file.exists():
468
+ legacy_file = self.storage.key_to_filepath(f"{self.name}.lndb")
469
+ if legacy_file.exists():
470
+ raise RuntimeError(
471
+ "The SQLite file has been renamed!\nPlease rename your SQLite file"
472
+ f" {legacy_file} to {self._sqlite_file}"
473
+ )
474
+ return False, f"SQLite file {self._sqlite_file} does not exist"
475
+ from lamindb_setup import settings # to check user
476
+
477
+ from .django import setup_django
478
+
479
+ # we need the local sqlite to setup django
480
+ self._update_local_sqlite_file(lock_cloud_sqlite=self._is_cloud_sqlite)
481
+ # setting up django also performs a check for migrations & prints them
482
+ # as warnings
483
+ # this should fail, e.g., if the db is not reachable
484
+ setup_django(self)
485
+ return True, ""
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+ from uuid import UUID, uuid4
5
+
6
+ from dotenv import dotenv_values
7
+ from lamin_utils import logger
8
+ from pydantic import ValidationError
9
+
10
+ from ._settings_instance import InstanceSettings
11
+ from ._settings_storage import StorageSettings
12
+ from ._settings_store import (
13
+ InstanceSettingsStore,
14
+ UserSettingsStore,
15
+ current_instance_settings_file,
16
+ current_user_settings_file,
17
+ system_storage_settings_file,
18
+ )
19
+ from ._settings_user import UserSettings
20
+
21
+ if TYPE_CHECKING:
22
+ from pathlib import Path
23
+
24
+
25
+ class SettingsEnvFileOutdated(Exception):
26
+ pass
27
+
28
+
29
+ def load_system_storage_settings(system_storage_settings: Path | None = None) -> dict:
30
+ if system_storage_settings is None:
31
+ system_storage_settings = system_storage_settings_file()
32
+
33
+ if system_storage_settings.exists():
34
+ return dotenv_values(system_storage_settings)
35
+ else:
36
+ return {}
37
+
38
+
39
+ def load_instance_settings(instance_settings_file: Path | None = None):
40
+ if instance_settings_file is None:
41
+ instance_settings_file = current_instance_settings_file()
42
+ if not instance_settings_file.exists():
43
+ raise SystemExit("No instance is loaded! Call `lamin init` or `lamin connect`")
44
+ try:
45
+ settings_store = InstanceSettingsStore(_env_file=instance_settings_file)
46
+ except (ValidationError, TypeError) as error:
47
+ with open(instance_settings_file) as f:
48
+ content = f.read()
49
+ raise SettingsEnvFileOutdated(
50
+ f"\n\n{error}\n\nYour instance settings file with\n\n{content}\nis invalid"
51
+ f" (likely outdated), see validation error. Please delete {instance_settings_file} &"
52
+ " reload (remote) or re-initialize (local) the instance with the same name & storage location."
53
+ ) from error
54
+ isettings = setup_instance_from_store(settings_store)
55
+ return isettings
56
+
57
+
58
+ def load_or_create_user_settings() -> UserSettings:
59
+ """Return current user settings."""
60
+ current_user_settings = current_user_settings_file()
61
+ if not current_user_settings.exists():
62
+ logger.warning("using anonymous user (to identify, call: lamin login)")
63
+ usettings = UserSettings(handle="anonymous", uid="00000000")
64
+ from ._settings_save import save_user_settings
65
+
66
+ save_user_settings(usettings)
67
+ else:
68
+ usettings = load_user_settings(current_user_settings)
69
+ return usettings
70
+
71
+
72
+ def load_user_settings(user_settings_file: Path):
73
+ try:
74
+ settings_store = UserSettingsStore(_env_file=user_settings_file)
75
+ except (ValidationError, TypeError) as error:
76
+ msg = (
77
+ "Your user settings file is invalid, please delete"
78
+ f" {user_settings_file} and log in again."
79
+ )
80
+ print(msg)
81
+ raise SettingsEnvFileOutdated(msg) from error
82
+ settings = setup_user_from_store(settings_store)
83
+ return settings
84
+
85
+
86
+ def _null_to_value(field, value=None):
87
+ return field if field != "null" else value
88
+
89
+
90
+ def setup_instance_from_store(store: InstanceSettingsStore) -> InstanceSettings:
91
+ ssettings = StorageSettings(
92
+ root=store.storage_root,
93
+ region=_null_to_value(store.storage_region),
94
+ )
95
+ return InstanceSettings(
96
+ id=UUID(store.id),
97
+ owner=store.owner,
98
+ name=store.name,
99
+ storage=ssettings,
100
+ db=_null_to_value(store.db),
101
+ schema=_null_to_value(store.schema_str),
102
+ git_repo=_null_to_value(store.git_repo),
103
+ keep_artifacts_local=store.keep_artifacts_local, # type: ignore
104
+ )
105
+
106
+
107
+ def setup_user_from_store(store: UserSettingsStore) -> UserSettings:
108
+ settings = UserSettings()
109
+ settings.email = _null_to_value(store.email)
110
+ settings.password = _null_to_value(store.password)
111
+ settings.access_token = store.access_token
112
+ settings.api_key = _null_to_value(store.api_key)
113
+ settings.uid = store.uid
114
+ settings.handle = _null_to_value(store.handle, value="anonymous")
115
+ settings.name = _null_to_value(store.name)
116
+ settings._uuid = UUID(store.uuid) if store.uuid != "null" else None
117
+ return settings
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Any, Optional, get_type_hints
5
+ from uuid import UUID
6
+
7
+ from ._settings_store import (
8
+ InstanceSettingsStore,
9
+ UserSettingsStore,
10
+ current_user_settings_file,
11
+ system_storage_settings_file,
12
+ user_settings_file_email,
13
+ user_settings_file_handle,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from ._settings_user import UserSettings
18
+ from .types import UPathStr
19
+
20
+
21
+ def save_user_settings(settings: UserSettings):
22
+ type_hints = get_type_hints(UserSettingsStore)
23
+ prefix = "lamin_user_"
24
+ save_settings(settings, current_user_settings_file(), type_hints, prefix)
25
+ if settings.email is not None:
26
+ save_settings(
27
+ settings, user_settings_file_email(settings.email), type_hints, prefix
28
+ )
29
+ if settings.handle is not None and settings.handle != "anonymous":
30
+ save_settings(
31
+ settings, user_settings_file_handle(settings.handle), type_hints, prefix
32
+ )
33
+
34
+
35
+ def save_settings(
36
+ settings: Any,
37
+ settings_file: Path,
38
+ type_hints: dict[str, Any],
39
+ prefix: str,
40
+ ):
41
+ with open(settings_file, "w") as f:
42
+ for store_key, type_ in type_hints.items():
43
+ if type_ == Optional[str]:
44
+ type_ = str
45
+ if type_ == Optional[bool]:
46
+ type_ = bool
47
+ if "__" not in store_key:
48
+ if store_key == "model_config":
49
+ continue
50
+ if store_key == "storage_root":
51
+ value = settings.storage.root_as_str
52
+ elif store_key == "storage_region":
53
+ value = settings.storage.region
54
+ else:
55
+ if store_key in {
56
+ "db",
57
+ "schema_str",
58
+ "name_",
59
+ "uuid",
60
+ "id",
61
+ "api_url",
62
+ "schema_id",
63
+ }:
64
+ settings_key = f"_{store_key.rstrip('_')}"
65
+ else:
66
+ settings_key = store_key
67
+ value = getattr(settings, settings_key, None)
68
+ if value is None:
69
+ value = "null"
70
+ elif isinstance(value, UUID):
71
+ value = value.hex
72
+ else:
73
+ value = type_(value)
74
+ f.write(f"{prefix}{store_key}={value}\n")
75
+
76
+
77
+ def save_instance_settings(settings: Any, settings_file: Path):
78
+ type_hints = get_type_hints(InstanceSettingsStore)
79
+ prefix = "lamindb_instance_"
80
+ save_settings(settings, settings_file, type_hints, prefix)
81
+
82
+
83
+ def save_system_storage_settings(
84
+ cache_path: UPathStr | None, settings_file: UPathStr | None = None
85
+ ):
86
+ cache_path = "null" if cache_path is None else cache_path
87
+ if isinstance(cache_path, Path): # also True for UPath
88
+ cache_path = cache_path.as_posix()
89
+ if settings_file is None:
90
+ settings_file = system_storage_settings_file()
91
+ with open(settings_file, "w") as f:
92
+ f.write(f"lamindb_cache_path={cache_path}")