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