lamindb_setup 1.6.1__py3-none-any.whl → 1.7.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.
@@ -12,7 +12,7 @@ Settings:
12
12
 
13
13
  """
14
14
 
15
- from . import django, types, upath
15
+ from . import django, upath
16
16
  from ._deprecated import deprecated
17
17
  from ._docs import doc_args
18
18
  from ._settings import SetupSettings
@@ -31,7 +31,7 @@ AWS_CREDENTIALS_EXPIRATION: int = 11 * 60 * 60 # refresh credentials after 11 h
31
31
 
32
32
  # set anon=True for these buckets if credentials fail for a public bucket
33
33
  # to be expanded
34
- PUBLIC_BUCKETS: tuple[str] = ("cellxgene-data-public",)
34
+ PUBLIC_BUCKETS: tuple[str, ...] = ("cellxgene-data-public", "bionty-assets")
35
35
 
36
36
 
37
37
  # s3-comaptible endpoints managed by lamin
@@ -79,8 +79,8 @@ class AWSOptionsManager:
79
79
  # use lamindata public bucket for this test
80
80
  fs.call_s3("head_bucket", Bucket="lamindata")
81
81
  self.anon_public = False
82
- except Exception as e:
83
- self.anon_public = isinstance(e, PermissionError)
82
+ except Exception:
83
+ self.anon_public = True
84
84
 
85
85
  def _find_root(self, path_str: str) -> str | None:
86
86
  roots = self._credentials_cache.keys()
@@ -5,7 +5,6 @@ import os
5
5
  from typing import Literal
6
6
  from urllib.request import urlretrieve
7
7
 
8
- from gotrue.errors import AuthUnknownError
9
8
  from lamin_utils import logger
10
9
  from pydantic_settings import BaseSettings
11
10
  from supabase import Client, create_client # type: ignore
@@ -26,7 +25,7 @@ def load_fallback_connector() -> Connector:
26
25
  return connector
27
26
 
28
27
 
29
- PROD_URL = "https://laesaummdydllppgfchu.supabase.co"
28
+ PROD_URL = "https://hub.lamin.ai"
30
29
  PROD_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxhZXNhdW1tZHlkbGxwcGdmY2h1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTY4NDA1NTEsImV4cCI6MTk3MjQxNjU1MX0.WUeCRiun0ExUxKIv5-CtjF6878H8u26t0JmCWx3_2-c"
31
30
 
32
31
 
@@ -173,7 +172,7 @@ def call_with_fallback(
173
172
  client = connect_hub(fallback_env=fallback_env)
174
173
  result = callable(**kwargs, client=client)
175
174
  break
176
- except AuthUnknownError as e:
175
+ except Exception as e:
177
176
  if fallback_env:
178
177
  raise e
179
178
  finally:
@@ -35,28 +35,36 @@ from ._hub_utils import (
35
35
  )
36
36
  from ._settings import settings
37
37
  from ._settings_instance import InstanceSettings
38
- from ._settings_storage import StorageSettings, base62
38
+ from ._settings_storage import StorageSettings, base62, instance_uid_from_uuid
39
39
  from .hashing import hash_and_encode_as_b62
40
40
 
41
41
  if TYPE_CHECKING:
42
42
  from supabase import Client # type: ignore
43
43
 
44
44
 
45
- def delete_storage_record(storage_uuid: UUID, access_token: str | None = None) -> None:
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
46
51
  return call_with_fallback_auth(
47
- _delete_storage_record, storage_uuid=storage_uuid, access_token=access_token
52
+ _delete_storage_record, storage_info=storage_info, access_token=access_token
48
53
  )
49
54
 
50
55
 
51
- def _delete_storage_record(storage_uuid: UUID, client: Client) -> None:
56
+ def _delete_storage_record(storage_info: dict[str, str], client: Client) -> None:
57
+ storage_uuid = UUID(storage_info["id"])
52
58
  if storage_uuid is None:
53
59
  return None
54
60
  response = client.table("storage").delete().eq("id", storage_uuid.hex).execute()
55
61
  if response.data:
56
- logger.important(f"deleted storage record on hub {storage_uuid.hex}")
62
+ logger.important(
63
+ f"deleted storage record on hub {storage_uuid.hex} | {storage_info['root']}"
64
+ )
57
65
  else:
58
66
  raise PermissionError(
59
- f"Deleting of storage with {storage_uuid.hex} was not successful. Probably, you"
67
+ f"Deleting of storage {storage_uuid.hex} ({storage_info['root']}) was not successful. Probably, you"
60
68
  " don't have sufficient permissions."
61
69
  )
62
70
 
@@ -94,21 +102,21 @@ def _select_storage(
94
102
  return False
95
103
  else:
96
104
  existing_storage = response.data[0]
97
- if existing_storage["instance_id"] is not None:
98
- if ssettings._instance_id is not None:
99
- # consider storage settings that are meant to be managed by an instance
100
- if UUID(existing_storage["instance_id"]) != ssettings._instance_id:
101
- # everything is alright if the instance_id matches
102
- # we're probably just switching storage locations
103
- # below can be turned into a warning and then delegate the error
104
- # to a unique constraint violation below
105
- raise ValueError(
106
- f"Storage root {root} is already managed by instance {existing_storage['instance_id']}."
107
- )
108
- else:
109
- # if the request is agnostic of the instance, that's alright,
110
- # we'll update the instance_id with what's stored in the hub
111
- ssettings._instance_id = UUID(existing_storage["instance_id"])
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"])
112
120
  ssettings._uuid_ = UUID(existing_storage["id"])
113
121
  if update_uid:
114
122
  ssettings._uid = existing_storage["lnid"]
@@ -122,9 +130,7 @@ def _select_storage_or_parent(path: str, client: Client) -> dict | None:
122
130
  if result["root"] is None:
123
131
  return None
124
132
  result["uid"] = result.pop("lnid")
125
- result["instance_uid"] = hash_and_encode_as_b62(
126
- UUID(result.pop("instance_id")).hex
127
- )[:12]
133
+ result["instance_uid"] = instance_uid_from_uuid(UUID(result.pop("instance_id")))
128
134
  return result
129
135
 
130
136
 
@@ -144,7 +150,9 @@ def init_storage_hub(
144
150
  auto_populate_instance: bool = True,
145
151
  created_by: UUID | None = None,
146
152
  access_token: str | None = None,
147
- ) -> Literal["hub-record-retrieved", "hub-record-created"]:
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."""
148
156
  if settings.user.handle != "anonymous" or access_token is not None:
149
157
  return call_with_fallback_auth(
150
158
  _init_storage_hub,
@@ -152,6 +160,7 @@ def init_storage_hub(
152
160
  auto_populate_instance=auto_populate_instance,
153
161
  created_by=created_by,
154
162
  access_token=access_token,
163
+ prevent_creation=prevent_creation,
155
164
  )
156
165
  else:
157
166
  storage_exists = call_with_fallback(
@@ -160,7 +169,7 @@ def init_storage_hub(
160
169
  if storage_exists:
161
170
  return "hub-record-retrieved"
162
171
  else:
163
- raise ValueError("Log in to create a storage location on the hub.")
172
+ return "hub-record-not-created"
164
173
 
165
174
 
166
175
  def _init_storage_hub(
@@ -168,7 +177,8 @@ def _init_storage_hub(
168
177
  ssettings: StorageSettings,
169
178
  auto_populate_instance: bool,
170
179
  created_by: UUID | None = None,
171
- ) -> Literal["hub-record-retrieved", "hub-record-created"]:
180
+ prevent_creation: bool = False,
181
+ ) -> Literal["hub-record-retrieved", "hub-record-created", "hub-record-not-created"]:
172
182
  from lamindb_setup import settings
173
183
 
174
184
  created_by = settings.user._uuid if created_by is None else created_by
@@ -177,6 +187,8 @@ def _init_storage_hub(
177
187
  root = ssettings.root_as_str
178
188
  if _select_storage(ssettings, update_uid=True, client=client):
179
189
  return "hub-record-retrieved"
190
+ if prevent_creation:
191
+ return "hub-record-not-created"
180
192
  if ssettings.type_is_cloud:
181
193
  id = uuid.uuid5(uuid.NAMESPACE_URL, root)
182
194
  else:
@@ -272,17 +284,11 @@ def _delete_instance(
272
284
  else:
273
285
  access_token = None
274
286
  root_path = create_path(root_string, access_token)
275
- mark_storage_root(
276
- root_path,
277
- storage_record["lnid"], # type: ignore
278
- ) # address permission error
279
287
  check_storage_is_empty(
280
288
  root_path, account_for_sqlite_file=account_for_sqlite_file
281
289
  )
282
- # first delete the storage records because we will turn instance_id on
283
- # storage into a FK soon
284
290
  for storage_record in storage_records:
285
- _delete_storage_record(UUID(storage_record["id"]), client) # type: ignore
291
+ _delete_storage_record(storage_record, client) # type: ignore
286
292
  _delete_instance_record(UUID(instance_with_storage["id"]), client)
287
293
  return None
288
294
 
@@ -361,6 +367,27 @@ def _connect_instance_hub(
361
367
  "get-instance-settings-v1",
362
368
  invoke_options={"body": {"owner": owner, "name": name}},
363
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"] # the instance was renamed
384
+ logger.warning(
385
+ f"'{owner}/{name}' was renamed, please use '{owner}/{new_name}'"
386
+ )
387
+ response = client.functions.invoke(
388
+ "get-instance-settings-v1",
389
+ invoke_options={"body": {"owner": owner, "name": new_name}},
390
+ )
364
391
  # no instance found, check why is that
365
392
  if response == b"{}":
366
393
  # try the via single requests, will take more time
@@ -375,7 +402,7 @@ def _connect_instance_hub(
375
402
  if storage is None:
376
403
  return "default-storage-does-not-exist-on-hub"
377
404
  logger.warning(
378
- "Could not find instance via API, but found directly querying hub."
405
+ "could not find instance via API, but found directly querying hub"
379
406
  )
380
407
  else:
381
408
  instance = json.loads(response)
@@ -467,6 +494,12 @@ def access_db(
467
494
  instance_id: UUID
468
495
  instance_slug: str
469
496
  instance_api_url: str | None
497
+ if (
498
+ "LAMIN_TEST_DB_TOKEN" in os.environ
499
+ and (env_db_token := os.environ["LAMIN_TEST_DB_TOKEN"]) != ""
500
+ ):
501
+ return env_db_token
502
+
470
503
  if isinstance(instance, InstanceSettings):
471
504
  instance_id = instance._id
472
505
  instance_slug = instance.slug
@@ -68,9 +68,23 @@ def select_instance_by_name(
68
68
  .execute()
69
69
  .data
70
70
  )
71
- if len(data) == 0:
72
- return None
73
- return data[0]
71
+ if len(data) != 0:
72
+ return data[0]
73
+
74
+ data = (
75
+ client.table("instance_previous_name")
76
+ .select(
77
+ "instance!instance_previous_name_instance_id_17ac5d61_fk_instance_id(*)"
78
+ )
79
+ .eq("instance.account_id", account_id)
80
+ .eq("previous_name", name)
81
+ .execute()
82
+ .data
83
+ )
84
+ if len(data) != 0:
85
+ return data[0]["instance"]
86
+
87
+ return None
74
88
 
75
89
 
76
90
  def select_instance_by_id(
@@ -4,29 +4,32 @@ import os
4
4
  import sys
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from appdirs import AppDirs
8
7
  from lamin_utils import logger
8
+ from platformdirs import user_cache_dir
9
9
 
10
10
  from ._settings_load import (
11
+ load_cache_path_from_settings,
11
12
  load_instance_settings,
12
13
  load_or_create_user_settings,
13
- load_system_storage_settings,
14
14
  )
15
- from ._settings_store import current_instance_settings_file, settings_dir
15
+ from ._settings_store import (
16
+ current_instance_settings_file,
17
+ settings_dir,
18
+ system_settings_dir,
19
+ )
16
20
  from .upath import LocalPathClasses, UPath
17
21
 
18
22
  if TYPE_CHECKING:
19
23
  from pathlib import Path
20
24
 
21
25
  from lamindb_setup.core import InstanceSettings, StorageSettings, UserSettings
22
-
23
- from .types import UPathStr
26
+ from lamindb_setup.types import UPathStr
24
27
 
25
28
 
26
- DEFAULT_CACHE_DIR = UPath(AppDirs("lamindb", "laminlabs").user_cache_dir)
29
+ DEFAULT_CACHE_DIR = UPath(user_cache_dir(appname="lamindb", appauthor="laminlabs"))
27
30
 
28
31
 
29
- def _process_cache_path(cache_path: UPathStr | None):
32
+ def _process_cache_path(cache_path: UPathStr | None) -> UPath | None:
30
33
  if cache_path is None or cache_path == "null":
31
34
  return None
32
35
  cache_dir = UPath(cache_path)
@@ -34,6 +37,8 @@ def _process_cache_path(cache_path: UPathStr | None):
34
37
  raise ValueError("cache dir should be a local path.")
35
38
  if cache_dir.exists() and not cache_dir.is_dir():
36
39
  raise ValueError("cache dir should be a directory.")
40
+ if not cache_dir.is_absolute():
41
+ raise ValueError("A path to the cache dir should be absolute.")
37
42
  return cache_dir
38
43
 
39
44
 
@@ -86,6 +91,17 @@ class SetupSettings:
86
91
  else:
87
92
  self._auto_connect_path.unlink(missing_ok=True)
88
93
 
94
+ @property
95
+ def is_connected(self) -> bool:
96
+ """Determine whether the current instance is fully connected and ready to use.
97
+
98
+ If `True`, the current instance is connected, meaning that the db and other settings
99
+ are properly configured for use.
100
+ """
101
+ from .django import IS_SETUP # always import to protect from assignment
102
+
103
+ return IS_SETUP
104
+
89
105
  @property
90
106
  def private_django_api(self) -> bool:
91
107
  """Turn internal Django API private to clean up the API (default `False`).
@@ -156,7 +172,7 @@ class SetupSettings:
156
172
  if "LAMIN_CACHE_DIR" in os.environ:
157
173
  cache_dir = UPath(os.environ["LAMIN_CACHE_DIR"])
158
174
  elif self._cache_dir is None:
159
- cache_path = load_system_storage_settings().get("lamindb_cache_path", None)
175
+ cache_path = load_cache_path_from_settings()
160
176
  cache_dir = _process_cache_path(cache_path)
161
177
  if cache_dir is None:
162
178
  cache_dir = DEFAULT_CACHE_DIR
@@ -189,10 +205,12 @@ class SetupSettings:
189
205
  # do not show current setting representation when building docs
190
206
  if "sphinx" in sys.modules:
191
207
  return object.__repr__(self)
192
- repr = self.user.__repr__()
193
- repr += f"\nAuto-connect in Python: {self.auto_connect}\n"
208
+ repr = f"Auto-connect in Python: {self.auto_connect}\n"
194
209
  repr += f"Private Django API: {self.private_django_api}\n"
195
210
  repr += f"Cache directory: {self.cache_dir.as_posix()}\n"
211
+ repr += f"User settings directory: {settings_dir.as_posix()}\n"
212
+ repr += f"System settings directory: {system_settings_dir.as_posix()}\n"
213
+ repr += self.user.__repr__() + "\n"
196
214
  if self._instance_exists:
197
215
  repr += self.instance.__repr__()
198
216
  else:
@@ -18,6 +18,7 @@ from ._settings_storage import (
18
18
  STORAGE_UID_FILE_KEY,
19
19
  StorageSettings,
20
20
  init_storage,
21
+ instance_uid_from_uuid,
21
22
  )
22
23
  from ._settings_store import current_instance_settings_file, instance_settings_file
23
24
  from .cloud_sqlite_locker import (
@@ -117,6 +118,8 @@ class InstanceSettings:
117
118
  else:
118
119
  db_print = value
119
120
  representation += f"\n- {attr}: {db_print}"
121
+ elif attr == "modules":
122
+ representation += f"\n- {attr}: {value if value else '{}'}"
120
123
  else:
121
124
  representation += f"\n- {attr}: {value}"
122
125
  return representation
@@ -165,7 +168,7 @@ class InstanceSettings:
165
168
  f"local storage location '{root_path}' is corrupted, cannot find marker file with storage uid"
166
169
  )
167
170
  try:
168
- uid = marker_path.read_text()
171
+ uid = marker_path.read_text().splitlines()[0]
169
172
  except PermissionError:
170
173
  logger.warning(
171
174
  f"ignoring the following location because no permission to read it: {marker_path}"
@@ -250,7 +253,9 @@ class InstanceSettings:
250
253
  return None
251
254
  local_root = UPath(local_root)
252
255
  assert isinstance(local_root, LocalPathClasses)
253
- self._storage_local, _ = init_storage(local_root, self._id, register_hub=True) # type: ignore
256
+ self._storage_local, _ = init_storage(
257
+ local_root, instance_id=self._id, instance_slug=self.slug, register_hub=True
258
+ ) # type: ignore
254
259
  register_storage_in_instance(self._storage_local) # type: ignore
255
260
  logger.important(f"defaulting to local storage: {self._storage_local.root}")
256
261
 
@@ -275,9 +280,7 @@ class InstanceSettings:
275
280
  @property
276
281
  def uid(self) -> str:
277
282
  """The user-facing instance id."""
278
- from .hashing import hash_and_encode_as_b62
279
-
280
- return hash_and_encode_as_b62(self._id.hex)[:12]
283
+ return instance_uid_from_uuid(self._id)
281
284
 
282
285
  @property
283
286
  def modules(self) -> set[str]:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from pathlib import Path
4
5
  from typing import TYPE_CHECKING
5
6
  from uuid import UUID, uuid4
6
7
 
@@ -8,6 +9,8 @@ from dotenv import dotenv_values
8
9
  from lamin_utils import logger
9
10
  from pydantic import ValidationError
10
11
 
12
+ from lamindb_setup.errors import SettingsEnvFileOutdated
13
+
11
14
  from ._settings_instance import InstanceSettings
12
15
  from ._settings_storage import StorageSettings
13
16
  from ._settings_store import (
@@ -15,26 +18,32 @@ from ._settings_store import (
15
18
  UserSettingsStore,
16
19
  current_instance_settings_file,
17
20
  current_user_settings_file,
18
- system_storage_settings_file,
21
+ platform_user_storage_settings_file,
22
+ system_settings_file,
19
23
  )
20
24
  from ._settings_user import UserSettings
21
25
 
22
- if TYPE_CHECKING:
23
- from pathlib import Path
24
-
25
-
26
- class SettingsEnvFileOutdated(Exception):
27
- pass
28
26
 
27
+ def load_cache_path_from_settings(storage_settings: Path | None = None) -> Path | None:
28
+ if storage_settings is None:
29
+ paltform_user_storage_settings = platform_user_storage_settings_file()
30
+ if paltform_user_storage_settings.exists():
31
+ cache_path = dotenv_values(paltform_user_storage_settings).get(
32
+ "lamindb_cache_path", None
33
+ )
34
+ else:
35
+ cache_path = None
29
36
 
30
- def load_system_storage_settings(system_storage_settings: Path | None = None) -> dict:
31
- if system_storage_settings is None:
32
- system_storage_settings = system_storage_settings_file()
37
+ if cache_path in {None, "null", ""}:
38
+ storage_settings = system_settings_file()
39
+ else:
40
+ return Path(cache_path)
33
41
 
34
- if system_storage_settings.exists():
35
- return dotenv_values(system_storage_settings)
42
+ if storage_settings.exists():
43
+ cache_path = dotenv_values(storage_settings).get("lamindb_cache_path", None)
44
+ return Path(cache_path) if cache_path not in {None, "null", ""} else None
36
45
  else:
37
- return {}
46
+ return None
38
47
 
39
48
 
40
49
  def load_instance_settings(instance_settings_file: Path | None = None):
@@ -113,7 +122,7 @@ def setup_instance_from_store(store: InstanceSettingsStore) -> InstanceSettings:
113
122
  git_repo=_null_to_value(store.git_repo),
114
123
  keep_artifacts_local=store.keep_artifacts_local, # type: ignore
115
124
  api_url=_null_to_value(store.api_url),
116
- schema_id=None if store.schema_id == "null" else UUID(store.schema_id),
125
+ schema_id=None if store.schema_id in {None, "null"} else UUID(store.schema_id),
117
126
  fine_grained_access=store.fine_grained_access,
118
127
  db_permissions=_null_to_value(store.db_permissions),
119
128
  )
@@ -8,14 +8,15 @@ from ._settings_store import (
8
8
  InstanceSettingsStore,
9
9
  UserSettingsStore,
10
10
  current_user_settings_file,
11
- system_storage_settings_file,
11
+ platform_user_storage_settings_file,
12
12
  user_settings_file_email,
13
13
  user_settings_file_handle,
14
14
  )
15
15
 
16
16
  if TYPE_CHECKING:
17
+ from lamindb_setup.types import UPathStr
18
+
17
19
  from ._settings_user import UserSettings
18
- from .types import UPathStr
19
20
 
20
21
 
21
22
  def save_user_settings(settings: UserSettings):
@@ -82,13 +83,13 @@ def save_instance_settings(settings: Any, settings_file: Path):
82
83
  save_settings(settings, settings_file, type_hints, prefix)
83
84
 
84
85
 
85
- def save_system_storage_settings(
86
+ def save_platform_user_storage_settings(
86
87
  cache_path: UPathStr | None, settings_file: UPathStr | None = None
87
88
  ):
88
89
  cache_path = "null" if cache_path is None else cache_path
89
90
  if isinstance(cache_path, Path): # also True for UPath
90
91
  cache_path = cache_path.as_posix()
91
92
  if settings_file is None:
92
- settings_file = system_storage_settings_file()
93
+ settings_file = platform_user_storage_settings_file()
93
94
  with open(settings_file, "w") as f:
94
95
  f.write(f"lamindb_cache_path={cache_path}")