lamindb_setup 1.9.1__py3-none-any.whl → 1.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. lamindb_setup/__init__.py +107 -107
  2. lamindb_setup/_cache.py +87 -87
  3. lamindb_setup/_check_setup.py +192 -166
  4. lamindb_setup/_connect_instance.py +415 -328
  5. lamindb_setup/_delete.py +144 -141
  6. lamindb_setup/_disconnect.py +35 -32
  7. lamindb_setup/_init_instance.py +430 -440
  8. lamindb_setup/_migrate.py +278 -266
  9. lamindb_setup/_register_instance.py +32 -35
  10. lamindb_setup/_schema_metadata.py +441 -441
  11. lamindb_setup/_set_managed_storage.py +69 -70
  12. lamindb_setup/_setup_user.py +172 -133
  13. lamindb_setup/core/__init__.py +21 -21
  14. lamindb_setup/core/_aws_options.py +223 -223
  15. lamindb_setup/core/_aws_storage.py +9 -1
  16. lamindb_setup/core/_hub_client.py +248 -248
  17. lamindb_setup/core/_hub_core.py +728 -665
  18. lamindb_setup/core/_hub_crud.py +227 -227
  19. lamindb_setup/core/_private_django_api.py +83 -83
  20. lamindb_setup/core/_settings.py +384 -377
  21. lamindb_setup/core/_settings_instance.py +577 -569
  22. lamindb_setup/core/_settings_load.py +141 -141
  23. lamindb_setup/core/_settings_save.py +95 -95
  24. lamindb_setup/core/_settings_storage.py +427 -429
  25. lamindb_setup/core/_settings_store.py +91 -91
  26. lamindb_setup/core/_settings_user.py +55 -55
  27. lamindb_setup/core/_setup_bionty_sources.py +44 -44
  28. lamindb_setup/core/cloud_sqlite_locker.py +240 -240
  29. lamindb_setup/core/django.py +315 -305
  30. lamindb_setup/core/exceptions.py +1 -1
  31. lamindb_setup/core/hashing.py +134 -134
  32. lamindb_setup/core/types.py +1 -1
  33. lamindb_setup/core/upath.py +1013 -1013
  34. lamindb_setup/errors.py +80 -70
  35. lamindb_setup/types.py +20 -20
  36. {lamindb_setup-1.9.1.dist-info → lamindb_setup-1.10.0.dist-info}/METADATA +3 -3
  37. lamindb_setup-1.10.0.dist-info/RECORD +50 -0
  38. lamindb_setup-1.9.1.dist-info/RECORD +0 -50
  39. {lamindb_setup-1.9.1.dist-info → lamindb_setup-1.10.0.dist-info}/LICENSE +0 -0
  40. {lamindb_setup-1.9.1.dist-info → lamindb_setup-1.10.0.dist-info}/WHEEL +0 -0
@@ -1,240 +1,240 @@
1
- from __future__ import annotations
2
-
3
- from datetime import datetime, timezone
4
- from functools import wraps
5
- from typing import TYPE_CHECKING
6
-
7
- from lamin_utils import logger
8
-
9
- from lamindb_setup.errors import InstanceLockedException
10
-
11
- from .upath import UPath, create_mapper, infer_filesystem
12
-
13
- if TYPE_CHECKING:
14
- from collections.abc import Callable
15
- from pathlib import Path
16
- from typing import ParamSpec, TypeVar
17
- from uuid import UUID
18
-
19
- from ._settings_instance import InstanceSettings
20
- from ._settings_user import UserSettings
21
-
22
- P = ParamSpec("P")
23
- R = TypeVar("R")
24
-
25
- EXPIRATION_TIME = 24 * 60 * 60 * 7 # 7 days
26
-
27
- MAX_MSG_COUNTER = 100 # print the msg after this number of iterations
28
-
29
-
30
- class empty_locker:
31
- has_lock = True
32
-
33
- @classmethod
34
- def lock(cls):
35
- pass
36
-
37
- @classmethod
38
- def unlock(cls):
39
- pass
40
-
41
-
42
- class Locker:
43
- def __init__(self, user_uid: str, storage_root: UPath | Path, instance_id: UUID):
44
- logger.debug(
45
- f"init cloud sqlite locker: {user_uid}, {storage_root}, {instance_id}."
46
- )
47
-
48
- self._counter = 0
49
-
50
- self.user = user_uid
51
- self.instance_id = instance_id
52
-
53
- self.root = storage_root
54
- self.fs, root_str = infer_filesystem(storage_root)
55
-
56
- exclusion_path = storage_root / f".lamindb/_exclusion/{instance_id.hex}"
57
-
58
- self.mapper = create_mapper(self.fs, str(exclusion_path), create=True)
59
-
60
- priorities_path = str(exclusion_path / "priorities")
61
- if self.fs.exists(priorities_path):
62
- self.users = self.mapper["priorities"].decode().split("*")
63
-
64
- if self.user not in self.users:
65
- self.priority = len(self.users)
66
- self.users.append(self.user)
67
- # potential problem here if 2 users join at the same time
68
- # can be avoided by using separate files for each user
69
- # and giving priority by timestamp
70
- # here writing the whole list back because gcs
71
- # does not support the append mode
72
- self.mapper["priorities"] = "*".join(self.users).encode()
73
- else:
74
- self.priority = self.users.index(self.user)
75
- else:
76
- self.mapper["priorities"] = self.user.encode()
77
- self.users = [self.user]
78
- self.priority = 0
79
-
80
- self.mapper[f"numbers/{self.user}"] = b"0"
81
- self.mapper[f"entering/{self.user}"] = b"0"
82
-
83
- # clean up failures
84
- for user in self.users:
85
- for endpoint in ("numbers", "entering"):
86
- user_endpoint = f"{endpoint}/{user}"
87
- user_path = str(exclusion_path / user_endpoint)
88
- if not self.fs.exists(user_path):
89
- continue
90
- if self.mapper[user_endpoint] == b"0":
91
- continue
92
- period = (datetime.now() - self.modified(user_path)).total_seconds()
93
- if period > EXPIRATION_TIME:
94
- logger.info(
95
- f"the lock of the user {user} seems to be stale, clearing"
96
- f" {endpoint}."
97
- )
98
- self.mapper[user_endpoint] = b"0"
99
-
100
- self._has_lock = None
101
- self._locked_by = None
102
-
103
- def modified(self, path):
104
- mtime = self.fs.modified(path)
105
- # always convert to the local timezone before returning
106
- # assume in utc if the time zone is not specified
107
- if mtime.tzinfo is None:
108
- mtime = mtime.replace(tzinfo=timezone.utc)
109
- return mtime.astimezone().replace(tzinfo=None)
110
-
111
- def _msg_on_counter(self, user):
112
- if self._counter == MAX_MSG_COUNTER:
113
- logger.warning(f"competing for the lock with the user {user}.")
114
-
115
- if self._counter <= MAX_MSG_COUNTER:
116
- self._counter += 1
117
-
118
- # Lamport's bakery algorithm
119
- def _lock_unsafe(self):
120
- if self._has_lock:
121
- return None
122
-
123
- self._has_lock = True
124
- self._locked_by = self.user
125
-
126
- self.users = self.mapper["priorities"].decode().split("*")
127
-
128
- self.mapper[f"entering/{self.user}"] = b"1"
129
-
130
- numbers = [int(self.mapper[f"numbers/{user}"]) for user in self.users]
131
- number = 1 + max(numbers)
132
- self.mapper[f"numbers/{self.user}"] = str(number).encode()
133
-
134
- self.mapper[f"entering/{self.user}"] = b"0"
135
-
136
- for i, user in enumerate(self.users):
137
- if i == self.priority:
138
- continue
139
-
140
- while self.mapper[f"entering/{user}"] == b"1":
141
- self._msg_on_counter(user)
142
-
143
- c_number = int(self.mapper[f"numbers/{user}"])
144
-
145
- if c_number == 0:
146
- continue
147
-
148
- if (number > c_number) or (number == c_number and self.priority > i):
149
- self._has_lock = False
150
- self._locked_by = user
151
- self.mapper[f"numbers/{self.user}"] = b"0"
152
- return None
153
-
154
- def lock(self):
155
- try:
156
- self._lock_unsafe()
157
- except BaseException as e:
158
- self.unlock()
159
- self._clear()
160
- raise e
161
-
162
- def unlock(self):
163
- self.mapper[f"numbers/{self.user}"] = b"0"
164
- self._has_lock = None
165
- self._locked_by = None
166
- self._counter = 0
167
-
168
- def _clear(self):
169
- self.mapper[f"entering/{self.user}"] = b"0"
170
-
171
- @property
172
- def has_lock(self):
173
- if self._has_lock is None:
174
- logger.info("the lock has not been initialized, trying to obtain the lock.")
175
- self.lock()
176
-
177
- return self._has_lock
178
-
179
-
180
- _locker: Locker | None = None
181
-
182
-
183
- def get_locker(
184
- isettings: InstanceSettings, usettings: UserSettings | None = None
185
- ) -> Locker:
186
- from ._settings import settings
187
-
188
- global _locker
189
-
190
- user_uid = settings.user.uid if usettings is None else usettings.uid
191
- storage_root = isettings.storage.root
192
-
193
- if (
194
- _locker is None
195
- or _locker.user != user_uid
196
- or _locker.root != storage_root
197
- or _locker.instance_id != isettings._id
198
- ):
199
- _locker = Locker(user_uid, storage_root, isettings._id)
200
-
201
- return _locker
202
-
203
-
204
- def clear_locker():
205
- global _locker
206
-
207
- _locker = None
208
-
209
-
210
- # decorator
211
- def unlock_cloud_sqlite_upon_exception(
212
- ignore_prev_locker: bool = False,
213
- ) -> Callable[[Callable[P, R]], Callable[P, R]]:
214
- """Decorator to unlock a cloud sqlite instance upon an exception.
215
-
216
- Ignores `InstanceLockedException`.
217
-
218
- Args:
219
- ignore_prev_locker: `bool` - Do not unlock if locker hasn't changed.
220
- """
221
-
222
- def wrap_with_args(func):
223
- # https://stackoverflow.com/questions/1782843/python-decorator-handling-docstrings
224
- @wraps(func)
225
- def wrapper(*args, **kwargs):
226
- prev_locker = _locker
227
- try:
228
- return func(*args, **kwargs)
229
- except Exception as exc:
230
- if isinstance(exc, InstanceLockedException):
231
- raise exc
232
- if ignore_prev_locker and _locker is prev_locker:
233
- raise exc
234
- if _locker is not None and _locker._has_lock:
235
- _locker.unlock()
236
- raise exc
237
-
238
- return wrapper
239
-
240
- return wrap_with_args
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime, timezone
4
+ from functools import wraps
5
+ from typing import TYPE_CHECKING
6
+
7
+ from lamin_utils import logger
8
+
9
+ from lamindb_setup.errors import InstanceLockedException
10
+
11
+ from .upath import UPath, create_mapper, infer_filesystem
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Callable
15
+ from pathlib import Path
16
+ from typing import ParamSpec, TypeVar
17
+ from uuid import UUID
18
+
19
+ from ._settings_instance import InstanceSettings
20
+ from ._settings_user import UserSettings
21
+
22
+ P = ParamSpec("P")
23
+ R = TypeVar("R")
24
+
25
+ EXPIRATION_TIME = 24 * 60 * 60 * 7 # 7 days
26
+
27
+ MAX_MSG_COUNTER = 100 # print the msg after this number of iterations
28
+
29
+
30
+ class empty_locker:
31
+ has_lock = True
32
+
33
+ @classmethod
34
+ def lock(cls):
35
+ pass
36
+
37
+ @classmethod
38
+ def unlock(cls):
39
+ pass
40
+
41
+
42
+ class Locker:
43
+ def __init__(self, user_uid: str, storage_root: UPath | Path, instance_id: UUID):
44
+ logger.debug(
45
+ f"init cloud sqlite locker: {user_uid}, {storage_root}, {instance_id}."
46
+ )
47
+
48
+ self._counter = 0
49
+
50
+ self.user = user_uid
51
+ self.instance_id = instance_id
52
+
53
+ self.root = storage_root
54
+ self.fs, root_str = infer_filesystem(storage_root)
55
+
56
+ exclusion_path = storage_root / f".lamindb/_exclusion/{instance_id.hex}"
57
+
58
+ self.mapper = create_mapper(self.fs, str(exclusion_path), create=True)
59
+
60
+ priorities_path = str(exclusion_path / "priorities")
61
+ if self.fs.exists(priorities_path):
62
+ self.users = self.mapper["priorities"].decode().split("*")
63
+
64
+ if self.user not in self.users:
65
+ self.priority = len(self.users)
66
+ self.users.append(self.user)
67
+ # potential problem here if 2 users join at the same time
68
+ # can be avoided by using separate files for each user
69
+ # and giving priority by timestamp
70
+ # here writing the whole list back because gcs
71
+ # does not support the append mode
72
+ self.mapper["priorities"] = "*".join(self.users).encode()
73
+ else:
74
+ self.priority = self.users.index(self.user)
75
+ else:
76
+ self.mapper["priorities"] = self.user.encode()
77
+ self.users = [self.user]
78
+ self.priority = 0
79
+
80
+ self.mapper[f"numbers/{self.user}"] = b"0"
81
+ self.mapper[f"entering/{self.user}"] = b"0"
82
+
83
+ # clean up failures
84
+ for user in self.users:
85
+ for endpoint in ("numbers", "entering"):
86
+ user_endpoint = f"{endpoint}/{user}"
87
+ user_path = str(exclusion_path / user_endpoint)
88
+ if not self.fs.exists(user_path):
89
+ continue
90
+ if self.mapper[user_endpoint] == b"0":
91
+ continue
92
+ period = (datetime.now() - self.modified(user_path)).total_seconds()
93
+ if period > EXPIRATION_TIME:
94
+ logger.info(
95
+ f"the lock of the user {user} seems to be stale, clearing"
96
+ f" {endpoint}."
97
+ )
98
+ self.mapper[user_endpoint] = b"0"
99
+
100
+ self._has_lock = None
101
+ self._locked_by = None
102
+
103
+ def modified(self, path):
104
+ mtime = self.fs.modified(path)
105
+ # always convert to the local timezone before returning
106
+ # assume in utc if the time zone is not specified
107
+ if mtime.tzinfo is None:
108
+ mtime = mtime.replace(tzinfo=timezone.utc)
109
+ return mtime.astimezone().replace(tzinfo=None)
110
+
111
+ def _msg_on_counter(self, user):
112
+ if self._counter == MAX_MSG_COUNTER:
113
+ logger.warning(f"competing for the lock with the user {user}.")
114
+
115
+ if self._counter <= MAX_MSG_COUNTER:
116
+ self._counter += 1
117
+
118
+ # Lamport's bakery algorithm
119
+ def _lock_unsafe(self):
120
+ if self._has_lock:
121
+ return None
122
+
123
+ self._has_lock = True
124
+ self._locked_by = self.user
125
+
126
+ self.users = self.mapper["priorities"].decode().split("*")
127
+
128
+ self.mapper[f"entering/{self.user}"] = b"1"
129
+
130
+ numbers = [int(self.mapper[f"numbers/{user}"]) for user in self.users]
131
+ number = 1 + max(numbers)
132
+ self.mapper[f"numbers/{self.user}"] = str(number).encode()
133
+
134
+ self.mapper[f"entering/{self.user}"] = b"0"
135
+
136
+ for i, user in enumerate(self.users):
137
+ if i == self.priority:
138
+ continue
139
+
140
+ while self.mapper[f"entering/{user}"] == b"1":
141
+ self._msg_on_counter(user)
142
+
143
+ c_number = int(self.mapper[f"numbers/{user}"])
144
+
145
+ if c_number == 0:
146
+ continue
147
+
148
+ if (number > c_number) or (number == c_number and self.priority > i):
149
+ self._has_lock = False
150
+ self._locked_by = user
151
+ self.mapper[f"numbers/{self.user}"] = b"0"
152
+ return None
153
+
154
+ def lock(self):
155
+ try:
156
+ self._lock_unsafe()
157
+ except BaseException as e:
158
+ self.unlock()
159
+ self._clear()
160
+ raise e
161
+
162
+ def unlock(self):
163
+ self.mapper[f"numbers/{self.user}"] = b"0"
164
+ self._has_lock = None
165
+ self._locked_by = None
166
+ self._counter = 0
167
+
168
+ def _clear(self):
169
+ self.mapper[f"entering/{self.user}"] = b"0"
170
+
171
+ @property
172
+ def has_lock(self):
173
+ if self._has_lock is None:
174
+ logger.info("the lock has not been initialized, trying to obtain the lock.")
175
+ self.lock()
176
+
177
+ return self._has_lock
178
+
179
+
180
+ _locker: Locker | None = None
181
+
182
+
183
+ def get_locker(
184
+ isettings: InstanceSettings, usettings: UserSettings | None = None
185
+ ) -> Locker:
186
+ from ._settings import settings
187
+
188
+ global _locker
189
+
190
+ user_uid = settings.user.uid if usettings is None else usettings.uid
191
+ storage_root = isettings.storage.root
192
+
193
+ if (
194
+ _locker is None
195
+ or _locker.user != user_uid
196
+ or _locker.root != storage_root
197
+ or _locker.instance_id != isettings._id
198
+ ):
199
+ _locker = Locker(user_uid, storage_root, isettings._id)
200
+
201
+ return _locker
202
+
203
+
204
+ def clear_locker():
205
+ global _locker
206
+
207
+ _locker = None
208
+
209
+
210
+ # decorator
211
+ def unlock_cloud_sqlite_upon_exception(
212
+ ignore_prev_locker: bool = False,
213
+ ) -> Callable[[Callable[P, R]], Callable[P, R]]:
214
+ """Decorator to unlock a cloud sqlite instance upon an exception.
215
+
216
+ Ignores `InstanceLockedException`.
217
+
218
+ Args:
219
+ ignore_prev_locker: `bool` - Do not unlock if locker hasn't changed.
220
+ """
221
+
222
+ def wrap_with_args(func):
223
+ # https://stackoverflow.com/questions/1782843/python-decorator-handling-docstrings
224
+ @wraps(func)
225
+ def wrapper(*args, **kwargs):
226
+ prev_locker = _locker
227
+ try:
228
+ return func(*args, **kwargs)
229
+ except Exception as exc:
230
+ if isinstance(exc, InstanceLockedException):
231
+ raise exc
232
+ if ignore_prev_locker and _locker is prev_locker:
233
+ raise exc
234
+ if _locker is not None and _locker._has_lock:
235
+ _locker.unlock()
236
+ raise exc
237
+
238
+ return wrapper
239
+
240
+ return wrap_with_args