lamindb_setup 0.69.5__py2.py3-none-any.whl → 0.71.0__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 (44) hide show
  1. lamindb_setup/__init__.py +15 -15
  2. lamindb_setup/_add_remote_storage.py +22 -33
  3. lamindb_setup/_cache.py +4 -1
  4. lamindb_setup/_check.py +3 -0
  5. lamindb_setup/_check_setup.py +13 -7
  6. lamindb_setup/_close.py +2 -0
  7. lamindb_setup/_connect_instance.py +50 -34
  8. lamindb_setup/_delete.py +121 -22
  9. lamindb_setup/_django.py +4 -1
  10. lamindb_setup/_exportdb.py +4 -2
  11. lamindb_setup/_importdb.py +5 -1
  12. lamindb_setup/_init_instance.py +58 -46
  13. lamindb_setup/_migrate.py +20 -14
  14. lamindb_setup/_register_instance.py +10 -3
  15. lamindb_setup/_schema.py +6 -3
  16. lamindb_setup/_setup_user.py +8 -8
  17. lamindb_setup/_silence_loggers.py +4 -2
  18. lamindb_setup/core/__init__.py +4 -3
  19. lamindb_setup/core/_aws_storage.py +3 -0
  20. lamindb_setup/core/_deprecated.py +2 -7
  21. lamindb_setup/core/_docs.py +2 -0
  22. lamindb_setup/core/_hub_client.py +12 -10
  23. lamindb_setup/core/_hub_core.py +206 -85
  24. lamindb_setup/core/_hub_crud.py +15 -11
  25. lamindb_setup/core/_hub_utils.py +11 -8
  26. lamindb_setup/core/_settings.py +23 -26
  27. lamindb_setup/core/_settings_instance.py +164 -42
  28. lamindb_setup/core/_settings_load.py +13 -8
  29. lamindb_setup/core/_settings_save.py +11 -8
  30. lamindb_setup/core/_settings_storage.py +104 -95
  31. lamindb_setup/core/_settings_store.py +3 -2
  32. lamindb_setup/core/_settings_user.py +10 -6
  33. lamindb_setup/core/_setup_bionty_sources.py +9 -2
  34. lamindb_setup/core/cloud_sqlite_locker.py +13 -10
  35. lamindb_setup/core/django.py +5 -0
  36. lamindb_setup/core/exceptions.py +4 -2
  37. lamindb_setup/core/hashing.py +15 -5
  38. lamindb_setup/core/types.py +5 -2
  39. lamindb_setup/core/upath.py +202 -92
  40. {lamindb_setup-0.69.5.dist-info → lamindb_setup-0.71.0.dist-info}/METADATA +6 -4
  41. lamindb_setup-0.71.0.dist-info/RECORD +43 -0
  42. lamindb_setup-0.69.5.dist-info/RECORD +0 -43
  43. {lamindb_setup-0.69.5.dist-info → lamindb_setup-0.71.0.dist-info}/LICENSE +0 -0
  44. {lamindb_setup-0.69.5.dist-info → lamindb_setup-0.71.0.dist-info}/WHEEL +0 -0
@@ -1,20 +1,26 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import shutil
3
5
  from pathlib import Path
4
- from typing import Literal, Optional, Set, Tuple
5
- from uuid import UUID
6
+ from typing import TYPE_CHECKING, Literal
7
+
8
+ from lamin_utils import logger
9
+
6
10
  from ._hub_client import call_with_fallback
7
11
  from ._hub_crud import select_account_handle_name_by_lnid
8
- from lamin_utils import logger
12
+ from ._hub_utils import LaminDsn, LaminDsnModel
13
+ from ._settings_save import save_instance_settings
14
+ from ._settings_storage import StorageSettings, init_storage, mark_storage_root
15
+ from ._settings_store import current_instance_settings_file, instance_settings_file
9
16
  from .cloud_sqlite_locker import (
10
- InstanceLockedException,
11
17
  EXPIRATION_TIME,
18
+ InstanceLockedException,
12
19
  )
13
- from ._hub_utils import LaminDsnModel, LaminDsn
14
- from ._settings_save import save_instance_settings
15
- from ._settings_storage import StorageSettings
16
- from ._settings_store import current_instance_settings_file, instance_settings_file
17
- from .upath import UPath
20
+ from .upath import LocalPathClasses, UPath, convert_pathlike
21
+
22
+ if TYPE_CHECKING:
23
+ from uuid import UUID
18
24
 
19
25
 
20
26
  def sanitize_git_repo_url(repo_url: str) -> str:
@@ -31,22 +37,28 @@ class InstanceSettings:
31
37
  owner: str, # owner handle
32
38
  name: str, # instance name
33
39
  storage: StorageSettings, # storage location
34
- uid: Optional[str] = None, # instance uid/lnid
35
- db: Optional[str] = None, # DB URI
36
- schema: Optional[str] = None, # comma-separated string of schema names
37
- git_repo: Optional[str] = None, # a git repo URL
40
+ keep_artifacts_local: bool = False, # default to local storage
41
+ uid: str | None = None, # instance uid/lnid
42
+ db: str | None = None, # DB URI
43
+ schema: str | None = None, # comma-separated string of schema names
44
+ git_repo: str | None = None, # a git repo URL
45
+ is_on_hub: bool = False, # initialized from hub
38
46
  ):
39
47
  from ._hub_utils import validate_db_arg
40
48
 
41
- self._id: UUID = id
49
+ self._id_: UUID = id
42
50
  self._owner: str = owner
43
51
  self._name: str = name
44
- self._uid: Optional[str] = uid
52
+ self._uid: str | None = uid
45
53
  self._storage: StorageSettings = storage
46
54
  validate_db_arg(db)
47
- self._db: Optional[str] = db
48
- self._schema_str: Optional[str] = schema
55
+ self._db: str | None = db
56
+ self._schema_str: str | None = schema
49
57
  self._git_repo = None if git_repo is None else sanitize_git_repo_url(git_repo)
58
+ # local storage
59
+ self._keep_artifacts_local = keep_artifacts_local
60
+ self._storage_local: StorageSettings | None = None
61
+ self._is_on_hub = is_on_hub
50
62
 
51
63
  def __repr__(self):
52
64
  """Rich string representation."""
@@ -85,14 +97,111 @@ class InstanceSettings:
85
97
  """Instance name."""
86
98
  return self._name
87
99
 
100
+ def _search_local_root(
101
+ self, local_root: str | None = None, mute_warning: bool = False
102
+ ) -> StorageSettings | None:
103
+ from lnschema_core.models import Storage
104
+
105
+ if local_root is not None:
106
+ local_records = Storage.objects.filter(root=local_root)
107
+ else:
108
+ local_records = Storage.objects.filter(type="local")
109
+ found = False
110
+ for record in local_records.all():
111
+ root_path = Path(record.root)
112
+ if root_path.exists():
113
+ marker_path = root_path / ".lamindb/_is_initialized"
114
+ if marker_path.exists():
115
+ uid = marker_path.read_text()
116
+ if uid == record.uid:
117
+ found = True
118
+ break
119
+ elif uid == "":
120
+ # legacy instance that was not yet marked properly
121
+ mark_storage_root(record.root, record.uid)
122
+ else:
123
+ continue
124
+ else:
125
+ # legacy instance that was not yet marked at all
126
+ mark_storage_root(record.root, record.uid)
127
+ break
128
+ if found:
129
+ return StorageSettings(record.root)
130
+ elif not mute_warning:
131
+ logger.warning(
132
+ f"none of the registered local storage locations were found in your environment: {local_records}"
133
+ "\n❗ please register a new local storage location via `ln.settings.storage_local = local_root_path` "
134
+ "and re-load/connect the instance"
135
+ )
136
+ return None
137
+
138
+ @property
139
+ def keep_artifacts_local(self) -> bool:
140
+ """Default to keeping artifacts local.
141
+
142
+ Enable this optional setting for cloud instances on lamin.ai.
143
+
144
+ Guide: :doc:`faq/keep-artifacts-local`
145
+ """
146
+ return self._keep_artifacts_local
147
+
88
148
  @property
89
- def identifier(self) -> str:
90
- """Unique semantic identifier."""
91
- logger.warning(
92
- "InstanceSettings.identifier is deprecated and will be removed, use"
93
- " InstanceSettings.slug instead"
149
+ def storage(self) -> StorageSettings:
150
+ """Default storage.
151
+
152
+ For a cloud instance, this is cloud storage. For a local instance, this
153
+ is a local directory.
154
+ """
155
+ return self._storage
156
+
157
+ @property
158
+ def storage_local(self) -> StorageSettings:
159
+ """An additional local default storage.
160
+
161
+ Is only available if :attr:`keep_artifacts_local` is enabled.
162
+
163
+ Guide: :doc:`faq/keep-artifacts-local`
164
+ """
165
+ if not self._keep_artifacts_local:
166
+ raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
167
+ if self._storage_local is None:
168
+ self._storage_local = self._search_local_root()
169
+ if self._storage_local is None:
170
+ # raise an error, there was a warning just before in search_local_root
171
+ raise ValueError()
172
+ return self._storage_local
173
+
174
+ @storage_local.setter
175
+ def storage_local(self, local_root: Path | str):
176
+ from lamindb_setup._init_instance import register_storage_in_instance
177
+
178
+ local_root = Path(local_root)
179
+ if not self._keep_artifacts_local:
180
+ raise ValueError("`keep_artifacts_local` is not enabled for this instance.")
181
+ storage_local = self._search_local_root(
182
+ local_root=StorageSettings(local_root).root_as_str, mute_warning=True
94
183
  )
95
- return self.slug
184
+ if storage_local is not None:
185
+ # great, we're merely switching storage location
186
+ self._storage_local = storage_local
187
+ logger.important(f"defaulting to local storage: {storage_local.root}")
188
+ return None
189
+ storage_local = self._search_local_root(mute_warning=True)
190
+ if storage_local is not None:
191
+ if os.getenv("LAMIN_TESTING") == "true":
192
+ response = "y"
193
+ else:
194
+ response = input(
195
+ "You already configured a local storage root for this instance in this"
196
+ f" environment: {self.storage_local.root}\nDo you want to register another one? (y/n)"
197
+ )
198
+ if response != "y":
199
+ return None
200
+ local_root = convert_pathlike(local_root)
201
+ assert isinstance(local_root, LocalPathClasses)
202
+ self._storage_local = init_storage(local_root, self._id, register_hub=True) # type: ignore
203
+ register_storage_in_instance(self._storage_local) # type: ignore
204
+ logger.important(f"defaulting to local storage: {self._storage_local.root}")
96
205
 
97
206
  @property
98
207
  def slug(self) -> str:
@@ -100,7 +209,7 @@ class InstanceSettings:
100
209
  return f"{self.owner}/{self.name}"
101
210
 
102
211
  @property
103
- def git_repo(self) -> Optional[str]:
212
+ def git_repo(self) -> str | None:
104
213
  """Sync transforms with scripts in git repository.
105
214
 
106
215
  Provide the full git repo URL.
@@ -108,17 +217,19 @@ class InstanceSettings:
108
217
  return self._git_repo
109
218
 
110
219
  @property
111
- def id(self) -> UUID:
220
+ def _id(self) -> UUID:
112
221
  """The internal instance id."""
113
- return self._id
222
+ return self._id_
114
223
 
115
224
  @property
116
- def uid(self) -> Optional[str]:
225
+ def uid(self) -> str:
117
226
  """The user-facing instance id."""
118
- return self._uid
227
+ from .hashing import hash_and_encode_as_b62
228
+
229
+ return hash_and_encode_as_b62(self._id.hex)[:12]
119
230
 
120
231
  @property
121
- def schema(self) -> Set[str]:
232
+ def schema(self) -> set[str]:
122
233
  """Schema modules in addition to core schema."""
123
234
  if self._schema_str is None:
124
235
  return {} # type: ignore
@@ -128,7 +239,7 @@ class InstanceSettings:
128
239
  @property
129
240
  def _sqlite_file(self) -> UPath:
130
241
  """SQLite file."""
131
- return self.storage.key_to_filepath(f"{self.id.hex}.lndb")
242
+ return self.storage.key_to_filepath(f"{self._id.hex}.lndb")
132
243
 
133
244
  @property
134
245
  def _sqlite_file_local(self) -> Path:
@@ -211,16 +322,12 @@ class InstanceSettings:
211
322
  assert self._db.startswith("postgresql"), f"Unexpected DB value: {self._db}"
212
323
  return "postgresql"
213
324
 
214
- @property
215
- def session(self):
216
- raise NotImplementedError
217
-
218
325
  @property
219
326
  def _is_cloud_sqlite(self) -> bool:
220
327
  # can we make this a private property, Sergei?
221
328
  # as it's not relevant to the user
222
329
  """Is this a cloud instance with sqlite db."""
223
- return self.dialect == "sqlite" and self.storage.is_cloud
330
+ return self.dialect == "sqlite" and self.storage.type_is_cloud
224
331
 
225
332
  @property
226
333
  def _cloud_sqlite_locker(self):
@@ -236,15 +343,10 @@ class InstanceSettings:
236
343
  else:
237
344
  return empty_locker
238
345
 
239
- @property
240
- def storage(self) -> StorageSettings:
241
- """Low-level access to storage location."""
242
- return self._storage
243
-
244
346
  @property
245
347
  def is_remote(self) -> bool:
246
348
  """Boolean indicating if an instance has no local component."""
247
- if not self.storage.is_cloud:
349
+ if not self.storage.type_is_cloud:
248
350
  return False
249
351
 
250
352
  def is_local_uri(uri: str):
@@ -263,6 +365,25 @@ class InstanceSettings:
263
365
  # and remote postgres
264
366
  return True
265
367
 
368
+ @property
369
+ def is_on_hub(self) -> bool:
370
+ """Is this instance on the hub.
371
+
372
+ Only works if user has access to the instance.
373
+ """
374
+ if self._is_on_hub is None:
375
+ from ._hub_client import call_with_fallback_auth
376
+ from ._hub_crud import select_instance_by_id
377
+
378
+ response = call_with_fallback_auth(
379
+ select_instance_by_id, instance_id=self._id.hex
380
+ )
381
+ if response is None:
382
+ self._is_on_hub = False
383
+ else:
384
+ self._is_on_hub = True
385
+ return self._is_on_hub
386
+
266
387
  def _get_settings_file(self) -> Path:
267
388
  return instance_settings_file(self.name, self.owner)
268
389
 
@@ -287,7 +408,7 @@ class InstanceSettings:
287
408
 
288
409
  def _load_db(
289
410
  self, do_not_lock_for_laminapp_admin: bool = False
290
- ) -> Tuple[bool, str]:
411
+ ) -> tuple[bool, str]:
291
412
  # Is the database available and initialized as LaminDB?
292
413
  # returns a tuple of status code and message
293
414
  if self.dialect == "sqlite" and not self._sqlite_file.exists():
@@ -299,6 +420,7 @@ class InstanceSettings:
299
420
  )
300
421
  return False, f"SQLite file {self._sqlite_file} does not exist"
301
422
  from lamindb_setup import settings # to check user
423
+
302
424
  from .django import setup_django
303
425
 
304
426
  # lock in all cases except if do_not_lock_for_laminapp_admin is True and
@@ -1,6 +1,8 @@
1
- from pathlib import Path
2
- from typing import Optional
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Optional
3
4
  from uuid import UUID, uuid4
5
+
4
6
  from lamin_utils import logger
5
7
  from pydantic.error_wrappers import ValidationError
6
8
 
@@ -14,12 +16,15 @@ from ._settings_store import (
14
16
  )
15
17
  from ._settings_user import UserSettings
16
18
 
19
+ if TYPE_CHECKING:
20
+ from pathlib import Path
21
+
17
22
 
18
23
  class SettingsEnvFileOutdated(Exception):
19
24
  pass
20
25
 
21
26
 
22
- def load_instance_settings(instance_settings_file: Optional[Path] = None):
27
+ def load_instance_settings(instance_settings_file: Path | None = None):
23
28
  if instance_settings_file is None:
24
29
  instance_settings_file = current_instance_settings_file()
25
30
  if not instance_settings_file.exists():
@@ -33,10 +38,10 @@ def load_instance_settings(instance_settings_file: Optional[Path] = None):
33
38
  f"\n\n{error}\n\nYour instance settings file with\n\n{content}\nis invalid"
34
39
  f" (likely outdated), please delete {instance_settings_file} &"
35
40
  " re-initialize (local) or re-connect to the instance (remote)"
36
- )
41
+ ) from error
37
42
  if settings_store.id == "null":
38
43
  raise ValueError(
39
- "Your instance.id is undefined, please either load your instance from the"
44
+ "Your instance._id is undefined, please either load your instance from the"
40
45
  f" hub or update {instance_settings_file} with a new id: {uuid4().hex}"
41
46
  )
42
47
  isettings = setup_instance_from_store(settings_store)
@@ -60,13 +65,13 @@ def load_or_create_user_settings() -> UserSettings:
60
65
  def load_user_settings(user_settings_file: Path):
61
66
  try:
62
67
  settings_store = UserSettingsStore(_env_file=user_settings_file)
63
- except (ValidationError, TypeError):
68
+ except (ValidationError, TypeError) as error:
64
69
  msg = (
65
70
  "Your user settings file is invalid, please delete"
66
71
  f" {user_settings_file} and log in again."
67
72
  )
68
73
  print(msg)
69
- raise SettingsEnvFileOutdated(msg)
74
+ raise SettingsEnvFileOutdated(msg) from error
70
75
  settings = setup_user_from_store(settings_store)
71
76
  return settings
72
77
 
@@ -95,5 +100,5 @@ def setup_user_from_store(store: UserSettingsStore) -> UserSettings:
95
100
  settings.uid = store.uid
96
101
  settings.handle = store.handle if store.handle != "null" else "anonymous"
97
102
  settings.name = store.name if store.name != "null" else None
98
- settings.uuid = UUID(store.uuid) if store.uuid != "null" else None
103
+ settings._uuid = UUID(store.uuid) if store.uuid != "null" else None
99
104
  return settings
@@ -1,17 +1,20 @@
1
- from pathlib import Path
1
+ from __future__ import annotations
2
2
 
3
- from typing import Any, Dict, Union, get_type_hints, Optional
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Any, Optional, get_type_hints
4
5
  from uuid import UUID
5
6
 
6
7
  from ._settings_store import (
7
- UserSettingsStore,
8
8
  InstanceSettingsStore,
9
+ UserSettingsStore,
9
10
  current_user_settings_file,
10
11
  user_settings_file_email,
11
12
  user_settings_file_handle,
12
13
  )
13
- from ._settings_user import UserSettings
14
- from .upath import UPath
14
+
15
+ if TYPE_CHECKING:
16
+ from ._settings_user import UserSettings
17
+ from .upath import UPath
15
18
 
16
19
 
17
20
  def save_user_settings(settings: UserSettings):
@@ -31,7 +34,7 @@ def save_user_settings(settings: UserSettings):
31
34
  def save_settings(
32
35
  settings: Any,
33
36
  settings_file: Path,
34
- type_hints: Dict[str, Any],
37
+ type_hints: dict[str, Any],
35
38
  prefix: str,
36
39
  ):
37
40
  with open(settings_file, "w") as f:
@@ -44,7 +47,7 @@ def save_settings(
44
47
  elif store_key == "storage_region":
45
48
  value = settings.storage.region
46
49
  else:
47
- if store_key in {"db", "schema_str", "name_"}:
50
+ if store_key in {"db", "schema_str", "name_", "uuid", "id"}:
48
51
  settings_key = f"_{store_key.rstrip('_')}"
49
52
  else:
50
53
  settings_key = store_key
@@ -65,7 +68,7 @@ def save_instance_settings(settings: Any, settings_file: Path):
65
68
 
66
69
 
67
70
  def save_system_storage_settings(
68
- cache_path: Union[str, Path, UPath, None], settings_file: Path
71
+ cache_path: str | Path | UPath | None, settings_file: Path
69
72
  ):
70
73
  cache_path = "null" if cache_path is None else cache_path
71
74
  if isinstance(cache_path, Path): # also True for UPath