lamindb_setup 1.19.0__py3-none-any.whl → 1.19.1__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 +1 -1
- lamindb_setup/_cache.py +87 -87
- lamindb_setup/_check.py +7 -7
- lamindb_setup/_check_setup.py +131 -131
- lamindb_setup/_connect_instance.py +443 -441
- lamindb_setup/_delete.py +155 -155
- lamindb_setup/_disconnect.py +38 -38
- lamindb_setup/_django.py +39 -39
- lamindb_setup/_entry_points.py +19 -19
- lamindb_setup/_init_instance.py +423 -423
- lamindb_setup/_migrate.py +331 -331
- lamindb_setup/_register_instance.py +32 -32
- lamindb_setup/_schema.py +27 -27
- lamindb_setup/_schema_metadata.py +451 -451
- lamindb_setup/_set_managed_storage.py +81 -81
- lamindb_setup/_setup_user.py +198 -198
- lamindb_setup/_silence_loggers.py +46 -46
- lamindb_setup/core/__init__.py +25 -34
- lamindb_setup/core/_aws_options.py +276 -276
- lamindb_setup/core/_aws_storage.py +57 -57
- lamindb_setup/core/_clone.py +50 -50
- lamindb_setup/core/_deprecated.py +62 -62
- lamindb_setup/core/_docs.py +14 -14
- lamindb_setup/core/_hub_client.py +288 -288
- lamindb_setup/core/_hub_crud.py +247 -247
- lamindb_setup/core/_hub_utils.py +100 -100
- lamindb_setup/core/_private_django_api.py +80 -80
- lamindb_setup/core/_settings.py +440 -434
- lamindb_setup/core/_settings_instance.py +22 -1
- lamindb_setup/core/_settings_load.py +162 -162
- lamindb_setup/core/_settings_save.py +108 -108
- lamindb_setup/core/_settings_storage.py +433 -433
- lamindb_setup/core/_settings_store.py +162 -162
- lamindb_setup/core/_settings_user.py +55 -55
- lamindb_setup/core/_setup_bionty_sources.py +44 -44
- lamindb_setup/core/cloud_sqlite_locker.py +240 -240
- lamindb_setup/core/django.py +414 -413
- lamindb_setup/core/exceptions.py +1 -1
- lamindb_setup/core/hashing.py +134 -134
- lamindb_setup/core/types.py +1 -1
- lamindb_setup/core/upath.py +1031 -1028
- lamindb_setup/errors.py +72 -72
- lamindb_setup/io.py +423 -423
- lamindb_setup/types.py +17 -17
- {lamindb_setup-1.19.0.dist-info → lamindb_setup-1.19.1.dist-info}/METADATA +3 -2
- lamindb_setup-1.19.1.dist-info/RECORD +51 -0
- {lamindb_setup-1.19.0.dist-info → lamindb_setup-1.19.1.dist-info}/WHEEL +1 -1
- {lamindb_setup-1.19.0.dist-info → lamindb_setup-1.19.1.dist-info/licenses}/LICENSE +201 -201
- lamindb_setup-1.19.0.dist-info/RECORD +0 -51
|
@@ -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
|