lamindb_setup 0.69.4__py2.py3-none-any.whl → 0.70.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.
lamindb_setup/__init__.py CHANGED
@@ -36,7 +36,7 @@ Modules & settings:
36
36
 
37
37
  """
38
38
 
39
- __version__ = "0.69.4" # denote a release candidate for 0.1.0 with 0.1rc1
39
+ __version__ = "0.70.0" # denote a release candidate for 0.1.0 with 0.1rc1
40
40
 
41
41
  import sys
42
42
  from os import name as _os_name
@@ -27,6 +27,17 @@ from ._migrate import check_whether_migrations_in_sync
27
27
  _TEST_FAILED_LOAD = False
28
28
 
29
29
 
30
+ INSTANCE_NOT_FOUND_MESSAGE = (
31
+ "'{owner}/{name}' not found:"
32
+ " '{hub_result}'\nCheck your permissions:"
33
+ " https://lamin.ai/{owner}/{name}"
34
+ )
35
+
36
+
37
+ class InstanceNotFoundError(SystemExit):
38
+ pass
39
+
40
+
30
41
  def check_db_dsn_equal_up_to_credentials(db_dsn_hub, db_dsn_local):
31
42
  return (
32
43
  db_dsn_hub.scheme == db_dsn_local.scheme
@@ -121,6 +132,8 @@ def connect(
121
132
  make_hub_request = True
122
133
  if settings_file.exists():
123
134
  isettings = load_instance_settings(settings_file)
135
+ # mimic instance_result from hub
136
+ instance_result = {"id": isettings.id.hex}
124
137
  # skip hub request for a purely local instance
125
138
  make_hub_request = isettings.is_remote
126
139
 
@@ -139,7 +152,6 @@ def connect(
139
152
  root=storage_result["root"],
140
153
  region=storage_result["region"],
141
154
  uid=storage_result["lnid"],
142
- is_hybrid=instance_result["storage_mode"] == "hybrid",
143
155
  )
144
156
  isettings = InstanceSettings(
145
157
  id=UUID(instance_result["id"]),
@@ -149,27 +161,24 @@ def connect(
149
161
  db=db_updated,
150
162
  schema=instance_result["schema_str"],
151
163
  git_repo=instance_result["git_repo"],
164
+ local_storage=instance_result["storage_mode"] == "hybrid",
152
165
  )
153
166
  check_whether_migrations_in_sync(instance_result["lamindb_version"])
154
167
  else:
155
- error_message = (
156
- f"'{owner}/{name}' not loadable:"
157
- f" '{hub_result}'\nCheck your permissions:"
158
- f" https://lamin.ai/{owner}/{name}?tab=collaborators"
168
+ message = INSTANCE_NOT_FOUND_MESSAGE.format(
169
+ owner=owner, name=name, hub_result=hub_result
159
170
  )
160
171
  if settings_file.exists():
161
172
  isettings = load_instance_settings(settings_file)
162
173
  if isettings.is_remote:
163
174
  if _raise_not_reachable_error:
164
- raise SystemExit(error_message)
175
+ raise InstanceNotFoundError(message)
165
176
  return "instance-not-reachable"
166
177
 
167
178
  else:
168
179
  if _raise_not_reachable_error:
169
- raise SystemExit(error_message)
180
+ raise InstanceNotFoundError(message)
170
181
  return "instance-not-reachable"
171
- # mimic instance_result from hub
172
- instance_result = {"id": isettings.id.hex}
173
182
 
174
183
  if storage is not None:
175
184
  update_isettings_with_storage(isettings, storage)
@@ -255,7 +264,7 @@ def update_isettings_with_storage(
255
264
  isettings: InstanceSettings, storage: UPathStr
256
265
  ) -> None:
257
266
  ssettings = StorageSettings(storage)
258
- if ssettings.is_cloud:
267
+ if ssettings.type_is_cloud:
259
268
  try: # triggering ssettings.id makes a lookup in the storage table
260
269
  logger.success(f"loaded storage: {ssettings.id} / {ssettings.root_as_str}")
261
270
  except RuntimeError as e:
lamindb_setup/_delete.py CHANGED
@@ -1,11 +1,17 @@
1
1
  import shutil
2
2
  from pathlib import Path
3
3
  from lamin_utils import logger
4
+ from uuid import UUID
4
5
  from typing import Optional
5
6
  from .core._settings_instance import InstanceSettings
7
+ from .core._settings_storage import StorageSettings
6
8
  from .core._settings import settings
7
9
  from .core._settings_load import load_instance_settings
8
10
  from .core._settings_store import instance_settings_file
11
+ from .core.upath import check_storage_is_empty, hosted_buckets
12
+ from .core._hub_core import delete_instance as delete_instance_on_hub
13
+ from .core._hub_core import connect_instance as load_instance_from_hub
14
+ from ._connect_instance import INSTANCE_NOT_FOUND_MESSAGE
9
15
 
10
16
 
11
17
  def delete_cache(cache_dir: Path):
@@ -13,6 +19,12 @@ def delete_cache(cache_dir: Path):
13
19
  shutil.rmtree(cache_dir)
14
20
 
15
21
 
22
+ def delete_exclusion_dir(isettings: InstanceSettings) -> None:
23
+ exclusion_dir = isettings.storage.root / f".lamindb/_exclusion/{isettings.id.hex}"
24
+ if exclusion_dir.exists():
25
+ exclusion_dir.rmdir()
26
+
27
+
16
28
  def delete_by_isettings(isettings: InstanceSettings) -> None:
17
29
  settings_file = isettings._get_settings_file()
18
30
  if settings_file.exists():
@@ -22,29 +34,31 @@ def delete_by_isettings(isettings: InstanceSettings) -> None:
22
34
  try:
23
35
  if isettings._sqlite_file.exists():
24
36
  isettings._sqlite_file.unlink()
25
- exclusion_dir = (
26
- isettings.storage.root / f".lamindb/_exclusion/{isettings.id.hex}"
27
- )
28
- if exclusion_dir.exists():
29
- exclusion_dir.rmdir()
30
37
  except PermissionError:
31
38
  logger.warning(
32
39
  "Did not have permission to delete SQLite file:"
33
40
  f" {isettings._sqlite_file}"
34
41
  )
35
42
  pass
36
- if isettings.is_remote:
37
- logger.warning("manually delete your remote instance on lamin.ai")
38
- logger.warning(f"manually delete your stored data: {isettings.storage.root}")
39
43
  # unset the global instance settings
40
44
  if settings._instance_exists and isettings.slug == settings.instance.slug:
41
45
  if settings._instance_settings_path.exists():
42
46
  settings._instance_settings_path.unlink()
43
47
  settings._instance_settings = None
48
+ if isettings.storage._mark_storage_root.exists():
49
+ isettings.storage._mark_storage_root.unlink()
44
50
 
45
51
 
46
- def delete(instance_name: str, force: bool = False) -> Optional[int]:
47
- """Delete a LaminDB instance."""
52
+ def delete(
53
+ instance_name: str, force: bool = False, require_empty: bool = True
54
+ ) -> Optional[int]:
55
+ """Delete a LaminDB instance.
56
+
57
+ Args:
58
+ instance_name (str): The name of the instance to delete.
59
+ force (bool): Whether to skip the confirmation prompt.
60
+ require_empty (bool): Whether to check if the instance is empty before deleting.
61
+ """
48
62
  if "/" in instance_name:
49
63
  logger.warning(
50
64
  "Deleting the instance of another user is currently not supported with the"
@@ -53,6 +67,44 @@ def delete(instance_name: str, force: bool = False) -> Optional[int]:
53
67
  )
54
68
  raise ValueError("Invalid instance name: '/' delimiter not allowed.")
55
69
  instance_slug = f"{settings.user.handle}/{instance_name}"
70
+ if settings._instance_exists and settings.instance.name == instance_name:
71
+ isettings = settings.instance
72
+ else:
73
+ settings_file = instance_settings_file(instance_name, settings.user.handle)
74
+ if not settings_file.exists():
75
+ hub_result = load_instance_from_hub(
76
+ owner=settings.user.handle, name=instance_name
77
+ )
78
+ if isinstance(hub_result, str):
79
+ message = INSTANCE_NOT_FOUND_MESSAGE.format(
80
+ owner=settings.user.handle,
81
+ name=instance_name,
82
+ hub_result=hub_result,
83
+ )
84
+ logger.warning(message)
85
+ return None
86
+ instance_result, storage_result = hub_result
87
+ ssettings = StorageSettings(
88
+ root=storage_result["root"],
89
+ region=storage_result["region"],
90
+ uid=storage_result["lnid"],
91
+ )
92
+ isettings = InstanceSettings(
93
+ id=UUID(instance_result["id"]),
94
+ owner=settings.user.handle,
95
+ name=instance_name,
96
+ storage=ssettings,
97
+ local_storage=instance_result["storage_mode"] == "hybrid",
98
+ db=instance_result["db"] if "db" in instance_result else None,
99
+ schema=instance_result["schema_str"],
100
+ git_repo=instance_result["git_repo"],
101
+ )
102
+ else:
103
+ isettings = load_instance_settings(settings_file)
104
+ if isettings.dialect != "sqlite":
105
+ logger.warning(
106
+ f"delete() does not yet affect your Postgres database at {isettings.db}"
107
+ )
56
108
  if not force:
57
109
  valid_responses = ["y", "yes"]
58
110
  user_input = (
@@ -62,17 +114,31 @@ def delete(instance_name: str, force: bool = False) -> Optional[int]:
62
114
  )
63
115
  if user_input not in valid_responses:
64
116
  return -1
65
- if settings._instance_exists and settings.instance.name == instance_name:
66
- isettings = settings.instance
67
- else:
68
- settings_file = instance_settings_file(instance_name, settings.user.handle)
69
- if not settings_file.exists():
117
+
118
+ # the actual deletion process begins here
119
+ if isettings.dialect == "sqlite" and isettings.is_remote:
120
+ # delete the exlusion dir first because it's hard to count its objects
121
+ delete_exclusion_dir(isettings)
122
+ if isettings.storage.type_is_cloud and isettings.storage.root_as_str.startswith(
123
+ hosted_buckets
124
+ ):
125
+ if not require_empty:
70
126
  logger.warning(
71
- "could not delete as instance settings do not exist locally. did you"
72
- " provide a wrong instance name? could you try loading it?"
127
+ "hosted storage always has to be empty, ignoring `require_empty`"
73
128
  )
74
- return None
75
- isettings = load_instance_settings(settings_file)
129
+ require_empty = True
130
+ n_objects = check_storage_is_empty(
131
+ isettings.storage.root,
132
+ raise_error=require_empty,
133
+ account_for_sqlite_file=isettings.dialect == "sqlite",
134
+ )
76
135
  logger.info(f"deleting instance {instance_slug}")
136
+ # below we can skip check_storage_is_empty() because we already called
137
+ # it above
138
+ delete_instance_on_hub(isettings.id, require_empty=False)
77
139
  delete_by_isettings(isettings)
140
+ if n_objects == 0 and isettings.storage.type == "local":
141
+ # dir is only empty after sqlite file was delete via delete_by_isettings
142
+ (isettings.storage.root / ".lamindb").rmdir()
143
+ isettings.storage.root.rmdir()
78
144
  return None
@@ -269,7 +269,7 @@ def init(
269
269
  delete_instance_record(isettings.id)
270
270
  isettings._get_settings_file().unlink(missing_ok=True) # type: ignore
271
271
  if ssettings is not None:
272
- delete_storage_record(ssettings.uuid) # type: ignore
272
+ delete_storage_record(ssettings._uuid) # type: ignore
273
273
  raise e
274
274
  return None
275
275
 
lamindb_setup/_migrate.py CHANGED
@@ -16,6 +16,9 @@ def check_whether_migrations_in_sync(db_version_str: str):
16
16
  installed_version_str = metadata.version("lamindb")
17
17
  except metadata.PackageNotFoundError:
18
18
  return None
19
+ if db_version_str is None:
20
+ logger.warning("no lamindb version stored to compare with installed version")
21
+ return None
19
22
  installed_version = version.parse(installed_version_str)
20
23
  db_version = version.parse(db_version_str)
21
24
  if (
@@ -83,7 +86,7 @@ class migrate:
83
86
  collaborator = call_with_fallback_auth(
84
87
  select_collaborator,
85
88
  instance_id=instance_id_str,
86
- account_id=settings.user.uuid,
89
+ account_id=settings.user._uuid,
87
90
  )
88
91
  if collaborator is None or collaborator["role"] != "admin":
89
92
  raise SystemExit(
@@ -17,6 +17,6 @@ def register(_test: bool = False):
17
17
  if ssettings._uid is None and _test:
18
18
  # because django isn't up, we can't get it from the database
19
19
  ssettings._uid = base62(8)
20
- ssettings._uuid = init_storage_hub(ssettings)
20
+ ssettings._uuid_ = init_storage_hub(ssettings)
21
21
  init_instance_hub(isettings)
22
22
  isettings._persist()
@@ -99,7 +99,7 @@ def login(
99
99
  user_settings.uid = user_id
100
100
  user_settings.handle = user_handle
101
101
  user_settings.name = user_name
102
- user_settings.uuid = user_uuid
102
+ user_settings._uuid = user_uuid
103
103
  user_settings.access_token = access_token
104
104
  save_user_settings(user_settings)
105
105
 
@@ -48,6 +48,14 @@ def _delete_storage_record(storage_uuid: UUID, client: Client) -> None:
48
48
  client.table("storage").delete().eq("id", storage_uuid.hex).execute()
49
49
 
50
50
 
51
+ def update_instance_record(instance_uuid: UUID, fields: Dict) -> None:
52
+ from ._hub_crud import update_instance
53
+
54
+ return call_with_fallback_auth(
55
+ update_instance, instance_id=instance_uuid.hex, instance_fields=fields
56
+ )
57
+
58
+
51
59
  def init_storage(
52
60
  ssettings: StorageSettings,
53
61
  ) -> UUID:
@@ -70,7 +78,7 @@ def _init_storage(ssettings: StorageSettings, client: Client) -> UUID:
70
78
  fields = dict(
71
79
  id=id.hex,
72
80
  lnid=ssettings.uid,
73
- created_by=settings.user.uuid.hex, # type: ignore
81
+ created_by=settings.user._uuid.hex, # type: ignore
74
82
  root=root,
75
83
  region=ssettings.region,
76
84
  type=ssettings.type,
@@ -81,13 +89,13 @@ def _init_storage(ssettings: StorageSettings, client: Client) -> UUID:
81
89
  return id
82
90
 
83
91
 
84
- def delete_instance(identifier: Union[UUID, str]):
92
+ def delete_instance(identifier: Union[UUID, str], require_empty: bool = True) -> None:
85
93
  """Fully delete an instance in the hub.
86
94
 
87
95
  This function deletes the relevant instance and storage records in the hub,
88
96
  conditional on the emptiness of the storage location.
89
97
  """
90
- from .upath import check_s3_storage_location_empty, create_path, hosted_buckets
98
+ from .upath import check_storage_is_empty, create_path
91
99
  from ._settings_storage import mark_storage_root
92
100
 
93
101
  if isinstance(identifier, UUID):
@@ -103,19 +111,22 @@ def delete_instance(identifier: Union[UUID, str]):
103
111
  name=name,
104
112
  )
105
113
 
106
- if instance_with_storage is not None:
114
+ if instance_with_storage is None:
115
+ logger.warning("instance not found")
116
+ return None
117
+
118
+ if require_empty:
107
119
  root_string = instance_with_storage["storage"]["root"]
108
120
  # gate storage and instance deletion on empty storage location for
109
- # both hosted and non-hosted s3 instances
110
- if root_string.startswith("s3://"):
111
- root_path = create_path(root_string)
112
- # only mark hosted instances
113
- if root_string.startswith(hosted_buckets):
114
- mark_storage_root(root_path)
115
- check_s3_storage_location_empty(root_path)
116
-
117
- delete_instance_record(UUID(instance_with_storage["id"]))
118
- delete_storage_record(UUID(instance_with_storage["storage_id"]))
121
+ root_path = create_path(root_string)
122
+ mark_storage_root(root_path) # address permission error
123
+ account_for_sqlite_file = instance_with_storage["db_scheme"] is None
124
+ check_storage_is_empty(
125
+ root_path, account_for_sqlite_file=account_for_sqlite_file
126
+ )
127
+ delete_instance_record(UUID(instance_with_storage["id"]))
128
+ delete_storage_record(UUID(instance_with_storage["storage_id"]))
129
+ return None
119
130
 
120
131
 
121
132
  def delete_instance_record(
@@ -140,9 +151,9 @@ def _init_instance(isettings: InstanceSettings, client: Client) -> None:
140
151
  lamindb_version = None
141
152
  fields = dict(
142
153
  id=isettings.id.hex,
143
- account_id=settings.user.uuid.hex, # type: ignore
154
+ account_id=settings.user._uuid.hex, # type: ignore
144
155
  name=isettings.name,
145
- storage_id=isettings.storage.uuid.hex, # type: ignore
156
+ storage_id=isettings.storage._uuid.hex, # type: ignore
146
157
  schema_str=isettings._schema_str,
147
158
  lamindb_version=lamindb_version,
148
159
  public=False,
@@ -1,5 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import shutil
5
+ from .upath import LocalPathClasses, convert_pathlike
3
6
  from pathlib import Path
4
7
  from typing import Literal, Optional, Set, Tuple
5
8
  from uuid import UUID
@@ -16,6 +19,11 @@ from ._settings_storage import StorageSettings
16
19
  from ._settings_store import current_instance_settings_file, instance_settings_file
17
20
  from .upath import UPath
18
21
 
22
+ LOCAL_STORAGE_ROOT_WARNING = (
23
+ "No local storage root found, set via `ln.setup.settings.instance.local_storage ="
24
+ " local_root`"
25
+ )
26
+
19
27
 
20
28
  def sanitize_git_repo_url(repo_url: str) -> str:
21
29
  assert repo_url.startswith("https://")
@@ -31,6 +39,7 @@ class InstanceSettings:
31
39
  owner: str, # owner handle
32
40
  name: str, # instance name
33
41
  storage: StorageSettings, # storage location
42
+ local_storage: bool = False, # default to local storage
34
43
  uid: Optional[str] = None, # instance uid/lnid
35
44
  db: Optional[str] = None, # DB URI
36
45
  schema: Optional[str] = None, # comma-separated string of schema names
@@ -47,6 +56,9 @@ class InstanceSettings:
47
56
  self._db: Optional[str] = db
48
57
  self._schema_str: Optional[str] = schema
49
58
  self._git_repo = None if git_repo is None else sanitize_git_repo_url(git_repo)
59
+ # local storage
60
+ self._local_storage_on = local_storage
61
+ self._local_storage = None
50
62
 
51
63
  def __repr__(self):
52
64
  """Rich string representation."""
@@ -85,14 +97,60 @@ class InstanceSettings:
85
97
  """Instance name."""
86
98
  return self._name
87
99
 
100
+ def _search_local_root(self):
101
+ from lnschema_core.models import Storage
102
+
103
+ records = Storage.objects.filter(type="local").all()
104
+ for record in records:
105
+ if Path(record.root).exists():
106
+ self._local_storage = StorageSettings(record.root)
107
+ logger.important(f"defaulting to local storage: {record}")
108
+ break
109
+ if self._local_storage is None:
110
+ logger.warning(LOCAL_STORAGE_ROOT_WARNING)
111
+
88
112
  @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"
94
- )
95
- return self.slug
113
+ def local_storage(self) -> StorageSettings:
114
+ """Default local storage.
115
+
116
+ Warning: Only enable if you're sure you want to use the more complicated
117
+ storage mode across local & cloud locations.
118
+
119
+ As an admin, enable via: `ln.setup.settings.instance.local_storage =
120
+ local_root`.
121
+
122
+ If enabled, you'll save artifacts to a default local storage
123
+ location at :attr:`~lamindb.setup.settings.InstanceSettings.local_storage`.
124
+
125
+ Upon passing `upload=True` in `artifact.save(upload=True)`, you upload the
126
+ artifact to the default cloud storage location:
127
+ :attr:`~lamindb.setup.core.InstanceSettings.storage`.
128
+ """
129
+ if not self._local_storage_on:
130
+ raise ValueError("Local storage is not enabled for this instance.")
131
+ if self._local_storage is None:
132
+ self._search_local_root()
133
+ if self._local_storage is None:
134
+ raise ValueError(LOCAL_STORAGE_ROOT_WARNING)
135
+ return self._local_storage
136
+
137
+ @local_storage.setter
138
+ def local_storage(self, local_root: Path):
139
+ from ._hub_core import update_instance_record
140
+ from .._init_instance import register_storage
141
+
142
+ self._search_local_root()
143
+ if self._local_storage is not None:
144
+ raise ValueError(
145
+ "You already configured a local storage root for this instance in this"
146
+ f" environment: {self.local_storage.root}"
147
+ )
148
+ local_root = convert_pathlike(local_root)
149
+ assert isinstance(local_root, LocalPathClasses)
150
+ self._local_storage = StorageSettings(local_root) # type: ignore
151
+ register_storage(self._local_storage) # type: ignore
152
+ self._local_storage_on = True
153
+ update_instance_record(self.id, {"storage_mode": "hybrid"})
96
154
 
97
155
  @property
98
156
  def slug(self) -> str:
@@ -211,16 +269,12 @@ class InstanceSettings:
211
269
  assert self._db.startswith("postgresql"), f"Unexpected DB value: {self._db}"
212
270
  return "postgresql"
213
271
 
214
- @property
215
- def session(self):
216
- raise NotImplementedError
217
-
218
272
  @property
219
273
  def _is_cloud_sqlite(self) -> bool:
220
274
  # can we make this a private property, Sergei?
221
275
  # as it's not relevant to the user
222
276
  """Is this a cloud instance with sqlite db."""
223
- return self.dialect == "sqlite" and self.storage.is_cloud
277
+ return self.dialect == "sqlite" and self.storage.type_is_cloud
224
278
 
225
279
  @property
226
280
  def _cloud_sqlite_locker(self):
@@ -244,7 +298,7 @@ class InstanceSettings:
244
298
  @property
245
299
  def is_remote(self) -> bool:
246
300
  """Boolean indicating if an instance has no local component."""
247
- if not self.storage.is_cloud:
301
+ if not self.storage.type_is_cloud:
248
302
  return False
249
303
 
250
304
  def is_local_uri(uri: str):
@@ -95,5 +95,5 @@ def setup_user_from_store(store: UserSettingsStore) -> UserSettings:
95
95
  settings.uid = store.uid
96
96
  settings.handle = store.handle if store.handle != "null" else "anonymous"
97
97
  settings.name = store.name if store.name != "null" else None
98
- settings.uuid = UUID(store.uuid) if store.uuid != "null" else None
98
+ settings._uuid = UUID(store.uuid) if store.uuid != "null" else None
99
99
  return settings
@@ -44,7 +44,7 @@ def save_settings(
44
44
  elif store_key == "storage_region":
45
45
  value = settings.storage.region
46
46
  else:
47
- if store_key in {"db", "schema_str", "name_"}:
47
+ if store_key in {"db", "schema_str", "name_", "uuid"}:
48
48
  settings_key = f"_{store_key.rstrip('_')}"
49
49
  else:
50
50
  settings_key = store_key
@@ -56,8 +56,9 @@ def get_storage_region(storage_root: UPathStr) -> Optional[str]:
56
56
 
57
57
 
58
58
  def mark_storage_root(root: UPathStr):
59
- # we need to touch a 0-byte object in the storage location to avoid
59
+ # we need to touch a 0-byte object in folder-like storage location on S3 to avoid
60
60
  # permission errors from leveraging s3fs on an empty hosted storage location
61
+ # for consistency, we write this file everywhere
61
62
  root_upath = convert_pathlike(root)
62
63
  mark_upath = root_upath / IS_INITIALIZED_KEY
63
64
  mark_upath.touch()
@@ -92,14 +93,13 @@ def init_storage(root: UPathStr) -> "StorageSettings":
92
93
  logger.error("`storage` is not a valid local, GCP storage or AWS S3 path")
93
94
  raise e
94
95
  ssettings = StorageSettings(uid=uid, root=root_str, region=region)
95
- if ssettings.is_cloud:
96
+ if ssettings.type_is_cloud:
96
97
  from ._hub_core import init_storage as init_storage_hub
97
98
 
98
99
  ssettings._description = f"Created as default storage for instance {uid}"
99
- ssettings._uuid = init_storage_hub(ssettings)
100
+ ssettings._uuid_ = init_storage_hub(ssettings)
100
101
  logger.important(f"registered storage: {ssettings.root_as_str}")
101
- if ssettings.is_cloud and root_str.startswith("create-s3"):
102
- mark_storage_root(ssettings.root)
102
+ mark_storage_root(ssettings.root)
103
103
  return ssettings
104
104
 
105
105
 
@@ -121,20 +121,17 @@ class StorageSettings:
121
121
  self,
122
122
  root: UPathStr,
123
123
  region: Optional[str] = None,
124
- is_hybrid: bool = False, # refers to storage mode
125
124
  uid: Optional[str] = None,
126
125
  uuid: Optional[UUID] = None,
127
126
  access_token: Optional[str] = None,
128
127
  ):
129
128
  self._uid = uid
130
- self._uuid = uuid
131
- self._is_hybrid = is_hybrid
129
+ self._uuid_ = uuid
132
130
  self._root_init = convert_pathlike(root)
133
131
  if isinstance(self._root_init, LocalPathClasses): # local paths
134
- self._root_init.mkdir(parents=True, exist_ok=True)
132
+ (self._root_init / ".lamindb").mkdir(parents=True, exist_ok=True)
135
133
  self._root_init = self._root_init.resolve()
136
134
  self._root = None
137
- self._remote_root = None
138
135
  self._aws_account_id: Optional[int] = None
139
136
  self._description: Optional[str] = None
140
137
  # we don't yet infer region here to make init fast
@@ -155,15 +152,19 @@ class StorageSettings:
155
152
  # save access_token here for use in self.root
156
153
  self.access_token = access_token
157
154
 
155
+ # local storage
156
+ self._has_local = False
157
+ self._local = None
158
+
158
159
  @property
159
160
  def id(self) -> int:
160
161
  """Storage id."""
161
162
  return self.record.id
162
163
 
163
164
  @property
164
- def uuid(self) -> Optional[UUID]:
165
- """Storage uuid."""
166
- return self._uuid
165
+ def _uuid(self) -> Optional[UUID]:
166
+ """Lamin's internal storage uuid."""
167
+ return self._uuid_
167
168
 
168
169
  @property
169
170
  def uid(self) -> Optional[str]:
@@ -172,6 +173,10 @@ class StorageSettings:
172
173
  self._uid = self.record.uid
173
174
  return self._uid
174
175
 
176
+ @property
177
+ def _mark_storage_root(self) -> UPath:
178
+ return self.root / IS_INITIALIZED_KEY
179
+
175
180
  @property
176
181
  def record(self) -> Any:
177
182
  """Storage record."""
@@ -180,63 +185,26 @@ class StorageSettings:
180
185
  from lnschema_core.models import Storage
181
186
  from ._settings import settings
182
187
 
183
- if not self.is_hybrid:
184
- self._record = Storage.objects.using(settings._using_key).get(
185
- root=self.root_as_str
186
- )
187
- else:
188
- # this has to be redone
189
- records = Storage.objects.filter(type="local").all()
190
- for record in records:
191
- if Path(record.root).exists():
192
- self._record = record
193
- logger.warning("found local storage location")
194
- break
188
+ self._record = Storage.objects.using(settings._using_key).get(
189
+ root=self.root_as_str
190
+ )
195
191
  return self._record
196
192
 
197
193
  def __repr__(self):
198
194
  """String rep."""
199
195
  s = f"root='{self.root_as_str}', uid='{self.uid}'"
200
- if self.uuid is not None:
201
- s += f", uuid='{self.uuid.hex}'"
196
+ if self._uuid is not None:
197
+ s += f", uuid='{self._uuid.hex}'"
202
198
  return f"StorageSettings({s})"
203
199
 
204
- @property
205
- def is_hybrid(self) -> bool:
206
- """Qualifies storage mode.
207
-
208
- A storage location can be local, in the cloud, or hybrid. See
209
- :attr:`~lamindb.setup.core.StorageSettings.type`.
210
-
211
- Hybrid means that a default local storage location is backed by an
212
- optional cloud storage location.
213
- """
214
- return self._is_hybrid
215
-
216
200
  @property
217
201
  def root(self) -> UPath:
218
202
  """Root storage location."""
219
203
  if self._root is None:
220
- if not self.is_hybrid:
221
- # below makes network requests to get credentials
222
- root_path = create_path(self._root_init, access_token=self.access_token)
223
- else:
224
- # this is a local path
225
- root_path = create_path(self.record.root)
226
- self._root = root_path
204
+ # below makes network requests to get credentials
205
+ self._root = create_path(self._root_init, access_token=self.access_token)
227
206
  return self._root
228
207
 
229
- @property
230
- def remote_root(self) -> UPath:
231
- """Remote storage location. Only needed for hybrid storage."""
232
- if not self.is_hybrid:
233
- raise ValueError("remote_root is only defined for hybrid storage")
234
- if self._remote_root is None:
235
- self._remote_root = create_path(
236
- self._root_init, access_token=self.access_token
237
- )
238
- return self._remote_root
239
-
240
208
  def _set_fs_kwargs(self, **kwargs):
241
209
  """Set additional fsspec arguments for cloud root.
242
210
 
@@ -299,7 +267,7 @@ class StorageSettings:
299
267
  raise e
300
268
 
301
269
  @property
302
- def is_cloud(self) -> bool:
270
+ def type_is_cloud(self) -> bool:
303
271
  """`True` if `storage_root` is in cloud, `False` otherwise."""
304
272
  return self.type != "local"
305
273
 
@@ -25,7 +25,7 @@ class UserSettings:
25
25
  """User access token."""
26
26
  uid: str = "null"
27
27
  """Universal user ID."""
28
- uuid: Optional[UUID] = None
28
+ _uuid: Optional[UUID] = None
29
29
  """Lamin's internal user ID."""
30
30
  name: Optional[str] = None
31
31
  """Full name."""
@@ -103,3 +103,6 @@ def setup_django(
103
103
 
104
104
  global IS_SETUP
105
105
  IS_SETUP = True
106
+
107
+ if isettings._local_storage_on:
108
+ isettings._search_local_root()
@@ -1,6 +1,8 @@
1
1
  # we are not documenting UPath here because it's documented at lamindb.UPath
2
2
  """Paths & file systems."""
3
3
 
4
+ from __future__ import annotations
5
+
4
6
  import os
5
7
  from datetime import datetime, timezone
6
8
  import botocore.session
@@ -165,7 +167,7 @@ class ProgressCallback(fsspec.callbacks.Callback):
165
167
  return None
166
168
 
167
169
 
168
- def download_to(self, path, print_progress: bool = False, **kwargs):
170
+ def download_to(self, path: UPathStr, print_progress: bool = False, **kwargs):
169
171
  """Download to a path."""
170
172
  if print_progress:
171
173
  # can't do path.is_dir() because path doesn't exist
@@ -180,20 +182,38 @@ def download_to(self, path, print_progress: bool = False, **kwargs):
180
182
  self.fs.download(str(self), str(path), **kwargs)
181
183
 
182
184
 
183
- def upload_from(self, path, print_progress: bool = False, **kwargs):
185
+ def upload_from(
186
+ self,
187
+ path: UPathStr,
188
+ dir_inplace: bool = False,
189
+ print_progress: bool = False,
190
+ **kwargs,
191
+ ):
184
192
  """Upload from a local path."""
193
+ path_is_dir = os.path.isdir(path)
194
+ if not path_is_dir:
195
+ dir_inplace = False
196
+
185
197
  if print_progress:
186
- if not os.path.isdir(path):
198
+ if not path_is_dir:
187
199
  cb = ProgressCallback("uploading")
188
200
  else:
189
201
  # todo: make proper progress bar for directories
190
202
  cb = fsspec.callbacks.NoOpCallback()
191
203
  kwargs["callback"] = cb
204
+
205
+ if dir_inplace:
206
+ path = Path(path)
207
+ source = [f for f in path.rglob("*") if f.is_file()]
208
+ destination = [str(self / f.relative_to(path)) for f in source]
209
+ source = [str(f) for f in source] # type: ignore
210
+ else:
211
+ source = str(path) # type: ignore
212
+ destination = str(self) # type: ignore
192
213
  # this weird thing is to avoid s3fs triggering create_bucket in upload
193
214
  # if dirs are present
194
215
  # it allows to avoid permission error
195
- destination = str(self)
196
- if self.protocol != "s3" or os.path.isfile(path):
216
+ if self.protocol != "s3" or not path_is_dir or dir_inplace:
197
217
  cleanup_cache = False
198
218
  else:
199
219
  bucket = self._url.netloc
@@ -205,7 +225,7 @@ def upload_from(self, path, print_progress: bool = False, **kwargs):
205
225
  else:
206
226
  cleanup_cache = False
207
227
 
208
- self.fs.upload(str(path), destination, **kwargs)
228
+ self.fs.upload(source, destination, **kwargs)
209
229
 
210
230
  if cleanup_cache:
211
231
  # normally this is invalidated after the upload but still better to check
@@ -670,20 +690,34 @@ class InstanceNotEmpty(Exception):
670
690
  pass
671
691
 
672
692
 
673
- def check_s3_storage_location_empty(root: UPathStr) -> None:
693
+ # is as fast as boto3: https://lamin.ai/laminlabs/lamindata/transform/krGp3hT1f78N5zKv
694
+ def check_storage_is_empty(
695
+ root: UPathStr, *, raise_error: bool = True, account_for_sqlite_file: bool = False
696
+ ) -> int:
674
697
  root_upath = convert_pathlike(root)
675
698
  root_string = root_upath.as_posix() # type: ignore
676
699
  # we currently touch a 0-byte file in the root of a hosted storage location
677
700
  # ({storage_root}/.lamindb/_is_initialized) during storage initialization
678
701
  # since path.fs.find raises a PermissionError on empty hosted
679
702
  # subdirectories (see lamindb_setup/core/_settings_storage/init_storage).
680
- n_touched_objects = 0
681
- if root_string.startswith(hosted_buckets):
682
- n_touched_objects = 1
703
+ n_offset_objects = 1 # because of touched dummy file, see mark_storage_root()
704
+ if account_for_sqlite_file:
705
+ n_offset_objects += 1 # because of SQLite file
683
706
  objects = root_upath.fs.find(root_string)
684
707
  n_objects = len(objects)
685
- if n_objects > n_touched_objects:
686
- raise InstanceNotEmpty(
687
- f"""storage location contains objects;
688
- {compute_file_tree(root_upath)[0]}"""
689
- )
708
+ n_diff = n_objects - n_offset_objects
709
+ ask_for_deletion = (
710
+ "delete them prior to deleting the instance"
711
+ if raise_error
712
+ else "consider deleting them"
713
+ )
714
+ message = (
715
+ f"Storage location contains {n_objects} objects "
716
+ f"({n_offset_objects} ignored) - {ask_for_deletion}\n{objects}"
717
+ )
718
+ if n_diff > 0:
719
+ if raise_error:
720
+ raise InstanceNotEmpty(message)
721
+ else:
722
+ logger.warning(message)
723
+ return n_diff
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: lamindb_setup
3
- Version: 0.69.4
3
+ Version: 0.70.0
4
4
  Summary: Setup & configure LaminDB.
5
5
  Author-email: Lamin Labs <laminlabs@gmail.com>
6
6
  Description-Content-Type: text/markdown
@@ -1,43 +1,43 @@
1
- lamindb_setup/__init__.py,sha256=0WvssKET9TeGkrbvOxtdFPzn4cpVXtv8JyRR4eGz7qU,1558
1
+ lamindb_setup/__init__.py,sha256=yTrf2HNpLb9hFr44mDAEjKjXDwrKYs_n2wX6JmyjOkQ,1558
2
2
  lamindb_setup/_add_remote_storage.py,sha256=S2rFvn-JcGaBOiBNQoAiaTPM590GQh-g4OJeNsben0Q,1802
3
3
  lamindb_setup/_cache.py,sha256=wrwlHi2IfxcWDfOW-cHzzkQNlLvasoQPPNxihICbwew,809
4
4
  lamindb_setup/_check.py,sha256=xgc5kF2HA4lGuAA9FJT9BJyQV5z1EwYMr92RTbGcQg4,92
5
5
  lamindb_setup/_check_setup.py,sha256=LLdTn4JYiyWKji_sexq2eENbmvA6_gdxqMKjDnF-nJc,2543
6
6
  lamindb_setup/_close.py,sha256=XtP7uyKRbwCGkXdSkwuiEnLYn5PEN3t3q50dPHNJRSU,1150
7
- lamindb_setup/_connect_instance.py,sha256=QICzNO3ID3JwJb2yMd6R5DfExT3q5vlkn0e8WHo2lLw,11789
8
- lamindb_setup/_delete.py,sha256=XzYaB2k6DhNfr5b-nzHZ4qGcAIxnuP8fc-hIOYXvy8E,3111
7
+ lamindb_setup/_connect_instance.py,sha256=XzHbYPmzPmAJZ1EaFtDBzw2skh1s0tppKCg2HOxx1HQ,11908
8
+ lamindb_setup/_delete.py,sha256=aIfQpOmb-icGsNVOd9EhHGGBQvsSJF7fBW_fzSwPhuM,5861
9
9
  lamindb_setup/_django.py,sha256=Yv-bNj80I5Fj9AJ2BFgcv9JZg4WUrlh3UStty82_jXQ,1500
10
10
  lamindb_setup/_exportdb.py,sha256=f5XUhRxTsU-yqfPM92OxjZKRxJo3lKaqYTRh9oytDfY,2084
11
11
  lamindb_setup/_importdb.py,sha256=bxFZNjtVOAW4yeMQakVYZE4-WNi4QE-MdMynHShMOgI,1836
12
- lamindb_setup/_init_instance.py,sha256=i0X9ENufdSGPog_8S1dypAvhwfGpVfsGuAIpnpfkjTc,11479
13
- lamindb_setup/_migrate.py,sha256=JlyjuhPO7qVEzaRfB5WBvkGgy-0qMh60MddM1zemSlo,8672
14
- lamindb_setup/_register_instance.py,sha256=tfdpL9NFXmtdxAGnym5Q4I5x4cT6_xVBim6pbECKlvk,795
12
+ lamindb_setup/_init_instance.py,sha256=DlVHpz8avTYaL-IoxSO4XM1rfZmtZXsWpjtY95vYmK0,11480
13
+ lamindb_setup/_migrate.py,sha256=wVDfIN1z2OIJztrenpYx42XN09retbPek49TzzmFQ6I,8810
14
+ lamindb_setup/_register_instance.py,sha256=1cZXXuCf6cqbiDchmW5vYNTf34wMNSZ2QtHmIJbTT4c,796
15
15
  lamindb_setup/_schema.py,sha256=edhbiUYatbO5FfBA6PYRmVmYWbtBXZBpATa70_MZiWo,642
16
- lamindb_setup/_setup_user.py,sha256=xwSJpbLtl2gW-frGs5k_kBL6PZZ00pe1gfmmd-a7myc,3657
16
+ lamindb_setup/_setup_user.py,sha256=5zZ924ckd8CGBiD4-5bdTN-NUNMS7N5w8Ovy6JIYPMU,3658
17
17
  lamindb_setup/_silence_loggers.py,sha256=auXnYVSx2ErZmyXZJ7FYYA69UQgUDQ5SnuznFXzfaWs,1548
18
18
  lamindb_setup/core/__init__.py,sha256=xfhu3cO_zPtNztmu6vlWtdbX92gPpnjyff5dOqCHTvE,369
19
19
  lamindb_setup/core/_aws_storage.py,sha256=GIT9SagV-1oFlLpRIogFgXuTwzMZsppFjqKA4-Vp66s,1762
20
20
  lamindb_setup/core/_deprecated.py,sha256=DNO_90uyYGM9on7rzJCu9atS7Z1qOQypsuL9PI6I-OQ,2491
21
21
  lamindb_setup/core/_docs.py,sha256=0ha3cZqzvIQnAXVC_JlAiRrFi3VpEEVookP_oURhBr4,240
22
22
  lamindb_setup/core/_hub_client.py,sha256=H09wJRck4d3fQ8VBSK1v8Dbj9RfqLdW9UqoTs6OCyUM,5520
23
- lamindb_setup/core/_hub_core.py,sha256=2ul3KvVorXLZfEefGbm9toRLga-rli8qzzq71vvEfw0,10990
23
+ lamindb_setup/core/_hub_core.py,sha256=Y_RnT82Y7fHgMmNvG8NTDTnx5Ybf_IwFQSMl6AFveVg,11294
24
24
  lamindb_setup/core/_hub_crud.py,sha256=0qKKsnHlx_FceX-jahdS2vJc2IuGRzouQrtAMn6MTSA,4505
25
25
  lamindb_setup/core/_hub_utils.py,sha256=4bjtm4QVivDwtVq3Bx9hQFZtdU2WmqrVdpupdvxMHcM,1862
26
26
  lamindb_setup/core/_settings.py,sha256=ogeOddaJ9tiolzpGwUIu6i4vhcViXQvsREEzZ95uyvI,3241
27
- lamindb_setup/core/_settings_instance.py,sha256=XRpHZJT8MZONpIS5Sop6Rk3BLYVmguh2PkC3TrGU7jU,11556
28
- lamindb_setup/core/_settings_load.py,sha256=65T_lW1xby__PKULufqzc1eWE30SVRNbxS2TAVSCWXM,3744
29
- lamindb_setup/core/_settings_save.py,sha256=bAnjmZuAKk4wx4EoTx-LfaCY7yyU_4bUfF7hdI71DgY,2563
30
- lamindb_setup/core/_settings_storage.py,sha256=a_Pa_nPzFuDeYxBlvFelQIkINlVGSNO_2aLvY_nxP-w,13044
27
+ lamindb_setup/core/_settings_instance.py,sha256=nAaTW4kd_wYM5hKlRwpLgBd-ATiEDvtHOX1NCnpkFyQ,13927
28
+ lamindb_setup/core/_settings_load.py,sha256=RkH_zfdGAlBO-4sovmFNGSP56XhSJpUYdvlZlNh-Qqs,3745
29
+ lamindb_setup/core/_settings_save.py,sha256=QnUQsYMAXv9xUYAsQ-5vldntSnAvNb3TMmJ61J3kU_I,2571
30
+ lamindb_setup/core/_settings_storage.py,sha256=Y2FsDmOFW7Kz1bh2MAMyQkWPJjoYN12ObpqVp82qRag,11750
31
31
  lamindb_setup/core/_settings_store.py,sha256=iM3Ei-YTSQo6wbcUhOliQx_KmHo8sxDx0-elJPt9AwQ,1929
32
- lamindb_setup/core/_settings_user.py,sha256=wWLrSaohWlQSLOA-7fLSRCV43rupy-ixNvkZMWT9Pgo,1281
32
+ lamindb_setup/core/_settings_user.py,sha256=ouRhbnIN12cjvVR0Iz98hUyYyGbqcGNRb1OCeI2T6wo,1282
33
33
  lamindb_setup/core/_setup_bionty_sources.py,sha256=xhIIJqtCzrjTONzU_68bq5kQxsNRDfAprHi2oJvY_LY,2908
34
34
  lamindb_setup/core/cloud_sqlite_locker.py,sha256=nFwAupnbIy_sxzlJBayfD2Dh8ZMUGNxbspIbv6BGrp8,6835
35
- lamindb_setup/core/django.py,sha256=MNlEFS8a7GX1IxmMgJymoRf3M03Eg881zQ2_MKONOQI,3335
35
+ lamindb_setup/core/django.py,sha256=bKwuv83ubKVsMeunnFtpWd1C-y29I2cuvpW10rCPpZo,3411
36
36
  lamindb_setup/core/exceptions.py,sha256=YzkJA51ZAa2w9lawAggSdg-NA-EIYJGNRnFd0DN3_bE,275
37
37
  lamindb_setup/core/hashing.py,sha256=nbQO_CSiX09O4bznHABft94fdFZAHPm2NIyvH5xagoo,2011
38
38
  lamindb_setup/core/types.py,sha256=fR71SLjoN1MkCjx8TJcjYDgmgO4bPXBX5J_RKpmmy_o,497
39
- lamindb_setup/core/upath.py,sha256=ahyWAQQfZRmtQBz-p_UN7R-wqsti6YZ8ZMixy-n8XlQ,24039
40
- lamindb_setup-0.69.4.dist-info/LICENSE,sha256=UOZ1F5fFDe3XXvG4oNnkL1-Ecun7zpHzRxjp-XsMeAo,11324
41
- lamindb_setup-0.69.4.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
42
- lamindb_setup-0.69.4.dist-info/METADATA,sha256=HKUfuOqFrwnVrQmc-wDO3_5rc4DNJMEu25L2BjPPKbo,1469
43
- lamindb_setup-0.69.4.dist-info/RECORD,,
39
+ lamindb_setup/core/upath.py,sha256=v8lPEA5j_SH0QzrHfw02x3HDt1ikFVc3gdoCCVfwfmw,25078
40
+ lamindb_setup-0.70.0.dist-info/LICENSE,sha256=UOZ1F5fFDe3XXvG4oNnkL1-Ecun7zpHzRxjp-XsMeAo,11324
41
+ lamindb_setup-0.70.0.dist-info/WHEEL,sha256=Sgu64hAMa6g5FdzHxXv9Xdse9yxpGGMeagVtPMWpJQY,99
42
+ lamindb_setup-0.70.0.dist-info/METADATA,sha256=9hjTjaw1xv6SQR_ugZg7DMKxGu0SqgJy0b9VJuR2rVw,1469
43
+ lamindb_setup-0.70.0.dist-info/RECORD,,