lamindb_setup 0.77.2__py2.py3-none-any.whl → 0.77.4__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 +444 -444
  7. lamindb_setup/_delete.py +9 -5
  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 +374 -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 +590 -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 +467 -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 +405 -393
  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 +232 -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.2.dist-info → lamindb_setup-0.77.4.dist-info}/METADATA +1 -1
  44. lamindb_setup-0.77.4.dist-info/RECORD +47 -0
  45. {lamindb_setup-0.77.2.dist-info → lamindb_setup-0.77.4.dist-info}/WHEEL +1 -1
  46. lamindb_setup-0.77.2.dist-info/RECORD +0 -47
  47. {lamindb_setup-0.77.2.dist-info → lamindb_setup-0.77.4.dist-info}/LICENSE +0 -0
@@ -1,232 +1,232 @@
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
+ 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