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

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