lamindb_setup 1.18.2__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 +4 -19
- 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 -438
- lamindb_setup/_delete.py +155 -151
- 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 -429
- lamindb_setup/_migrate.py +331 -327
- 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 -80
- 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 -266
- lamindb_setup/core/_aws_storage.py +57 -55
- 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 -294
- lamindb_setup/core/_hub_core.py +0 -2
- 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 +32 -7
- lamindb_setup/core/_settings_load.py +162 -159
- lamindb_setup/core/_settings_save.py +108 -96
- lamindb_setup/core/_settings_storage.py +433 -433
- lamindb_setup/core/_settings_store.py +162 -92
- 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 -70
- lamindb_setup/io.py +423 -416
- lamindb_setup/types.py +17 -17
- {lamindb_setup-1.18.2.dist-info → lamindb_setup-1.19.1.dist-info}/METADATA +4 -2
- lamindb_setup-1.19.1.dist-info/RECORD +51 -0
- {lamindb_setup-1.18.2.dist-info → lamindb_setup-1.19.1.dist-info}/WHEEL +1 -1
- {lamindb_setup-1.18.2.dist-info → lamindb_setup-1.19.1.dist-info/licenses}/LICENSE +201 -201
- lamindb_setup-1.18.2.dist-info/RECORD +0 -51
lamindb_setup/core/django.py
CHANGED
|
@@ -1,413 +1,414 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
# flake8: noqa
|
|
4
|
-
import builtins
|
|
5
|
-
import os
|
|
6
|
-
import sys
|
|
7
|
-
import importlib as il
|
|
8
|
-
import gzip
|
|
9
|
-
import jwt
|
|
10
|
-
import time
|
|
11
|
-
import threading
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
import shutil
|
|
14
|
-
from packaging import version
|
|
15
|
-
from ._settings_instance import InstanceSettings, is_local_db_url
|
|
16
|
-
from ..errors import CurrentInstanceNotConfigured
|
|
17
|
-
from lamin_utils import logger
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
IS_RUN_FROM_IPYTHON = getattr(builtins, "__IPYTHON__", False)
|
|
21
|
-
IS_SETUP = False
|
|
22
|
-
IS_MIGRATING = False
|
|
23
|
-
CONN_MAX_AGE = 299
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def get_connection(connection_name: str):
|
|
27
|
-
from django.db import connections
|
|
28
|
-
|
|
29
|
-
return connections[connection_name]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def error_no_instance_wrapper(execute, sql, params, many, context):
|
|
33
|
-
connection = context["connection"]
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
connection.vendor == "sqlite"
|
|
37
|
-
and connection.settings_dict.get("NAME") == ":memory:"
|
|
38
|
-
):
|
|
39
|
-
raise CurrentInstanceNotConfigured
|
|
40
|
-
|
|
41
|
-
return execute(sql, params, many, context)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# db token that refreshes on access if needed
|
|
45
|
-
class DBToken:
|
|
46
|
-
def __init__(
|
|
47
|
-
self, instance: InstanceSettings | dict, access_token: str | None = None
|
|
48
|
-
):
|
|
49
|
-
self.instance = instance
|
|
50
|
-
self.access_token = access_token
|
|
51
|
-
# initialized in token_query
|
|
52
|
-
self._token: str | None = None
|
|
53
|
-
self._token_query: str | None = None
|
|
54
|
-
self._expiration: float
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
from
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
self.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
from django.db.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# psycopg3
|
|
102
|
-
# psycopg3
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
and
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
#
|
|
143
|
-
connection
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
import
|
|
202
|
-
|
|
203
|
-
from django.
|
|
204
|
-
from django.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
installed_apps
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
"
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"
|
|
284
|
-
|
|
285
|
-
"
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
"
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
"
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
# the first time, it
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
and
|
|
358
|
-
and
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
IS_MIGRATING
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
IS_SETUP
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
#
|
|
377
|
-
#
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
from django.
|
|
381
|
-
from django.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
apps.
|
|
399
|
-
apps.
|
|
400
|
-
apps.
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
IS_SETUP
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
# flake8: noqa
|
|
4
|
+
import builtins
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import importlib as il
|
|
8
|
+
import gzip
|
|
9
|
+
import jwt
|
|
10
|
+
import time
|
|
11
|
+
import threading
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import shutil
|
|
14
|
+
from packaging import version
|
|
15
|
+
from ._settings_instance import InstanceSettings, is_local_db_url
|
|
16
|
+
from ..errors import CurrentInstanceNotConfigured
|
|
17
|
+
from lamin_utils import logger
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
IS_RUN_FROM_IPYTHON = getattr(builtins, "__IPYTHON__", False)
|
|
21
|
+
IS_SETUP = False
|
|
22
|
+
IS_MIGRATING = False
|
|
23
|
+
CONN_MAX_AGE = 299
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_connection(connection_name: str):
|
|
27
|
+
from django.db import connections
|
|
28
|
+
|
|
29
|
+
return connections[connection_name]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def error_no_instance_wrapper(execute, sql, params, many, context):
|
|
33
|
+
connection = context["connection"]
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
connection.vendor == "sqlite"
|
|
37
|
+
and connection.settings_dict.get("NAME") == ":memory:"
|
|
38
|
+
):
|
|
39
|
+
raise CurrentInstanceNotConfigured
|
|
40
|
+
|
|
41
|
+
return execute(sql, params, many, context)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# db token that refreshes on access if needed
|
|
45
|
+
class DBToken:
|
|
46
|
+
def __init__(
|
|
47
|
+
self, instance: InstanceSettings | dict, access_token: str | None = None
|
|
48
|
+
):
|
|
49
|
+
self.instance = instance
|
|
50
|
+
self.access_token = access_token
|
|
51
|
+
# initialized in token_query
|
|
52
|
+
self._token: str | None = None
|
|
53
|
+
self._token_query: str | None = None
|
|
54
|
+
self._expiration: float | None = None
|
|
55
|
+
self._type: str | None = None
|
|
56
|
+
|
|
57
|
+
def _refresh_token(self):
|
|
58
|
+
from ._hub_core import access_db
|
|
59
|
+
from psycopg2.extensions import adapt
|
|
60
|
+
|
|
61
|
+
self._token = access_db(self.instance, self.access_token)
|
|
62
|
+
self._token_query = (
|
|
63
|
+
f"SELECT set_token({adapt(self._token).getquoted().decode()}, true);"
|
|
64
|
+
)
|
|
65
|
+
token_decoded = jwt.decode(self._token, options={"verify_signature": False})
|
|
66
|
+
self._expiration = token_decoded["exp"]
|
|
67
|
+
self._type = token_decoded["type"]
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def token_query(self) -> str:
|
|
71
|
+
# refresh token if needed
|
|
72
|
+
if self._token is None or time.time() >= self._expiration: # type: ignore
|
|
73
|
+
self._refresh_token()
|
|
74
|
+
|
|
75
|
+
return self._token_query # type: ignore
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# a class to manage jwt in dbs
|
|
79
|
+
class DBTokenManager:
|
|
80
|
+
def __init__(self):
|
|
81
|
+
from django.db.transaction import Atomic
|
|
82
|
+
|
|
83
|
+
self.original_atomic_enter = Atomic.__enter__
|
|
84
|
+
self.atomic_is_patched = False
|
|
85
|
+
|
|
86
|
+
self.tokens: dict[str, DBToken] = {}
|
|
87
|
+
|
|
88
|
+
def set(self, token: DBToken, connection_name: str = "default"):
|
|
89
|
+
if connection_name in self.tokens:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
from django.db.transaction import Atomic
|
|
93
|
+
from django.db.backends.signals import connection_created
|
|
94
|
+
|
|
95
|
+
def set_token_wrapper(execute, sql, params, many, context):
|
|
96
|
+
not_in_atomic_block = not context["connection"].in_atomic_block
|
|
97
|
+
# ignore atomic blocks
|
|
98
|
+
if not_in_atomic_block:
|
|
99
|
+
sql = token.token_query + sql
|
|
100
|
+
result = execute(sql, params, many, context)
|
|
101
|
+
# this ensures that psycopg3 in the current env doesn't break this wrapper
|
|
102
|
+
# psycopg3 returns a cursor
|
|
103
|
+
# psycopg3 fetching differs from psycopg2, it returns the output of all sql statements
|
|
104
|
+
# not only the last one as psycopg2 does. So we shift the cursor from set_token
|
|
105
|
+
if (
|
|
106
|
+
not_in_atomic_block
|
|
107
|
+
and result is not None
|
|
108
|
+
and hasattr(result, "nextset")
|
|
109
|
+
):
|
|
110
|
+
result.nextset()
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
get_connection(connection_name).execute_wrappers.append(set_token_wrapper)
|
|
114
|
+
|
|
115
|
+
def connection_callback(sender, connection, **kwargs):
|
|
116
|
+
if (
|
|
117
|
+
connection.alias == connection_name
|
|
118
|
+
and set_token_wrapper not in connection.execute_wrappers
|
|
119
|
+
):
|
|
120
|
+
connection.execute_wrappers.append(set_token_wrapper)
|
|
121
|
+
|
|
122
|
+
dispatch_uid = f"dbtokenmanager:{id(self)}:{connection_name}"
|
|
123
|
+
# emitted when a database connection is established
|
|
124
|
+
# not when a database wrapper is created
|
|
125
|
+
connection_created.connect(
|
|
126
|
+
connection_callback, dispatch_uid=dispatch_uid, weak=False
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
self.tokens[connection_name] = token
|
|
130
|
+
|
|
131
|
+
if not self.atomic_is_patched:
|
|
132
|
+
# ensure we set the token only once for an outer atomic block
|
|
133
|
+
def __enter__(atomic):
|
|
134
|
+
self.original_atomic_enter(atomic)
|
|
135
|
+
connection_name = "default" if atomic.using is None else atomic.using
|
|
136
|
+
if connection_name in self.tokens:
|
|
137
|
+
# here we don't use the connection from the closure
|
|
138
|
+
# because Atomic is a single class to manage transactions for all connections
|
|
139
|
+
connection = get_connection(connection_name)
|
|
140
|
+
if len(connection.atomic_blocks) == 1:
|
|
141
|
+
token = self.tokens[connection_name]
|
|
142
|
+
# use raw psycopg2 connection here
|
|
143
|
+
# atomic block ensures connection
|
|
144
|
+
connection.connection.cursor().execute(token.token_query)
|
|
145
|
+
|
|
146
|
+
Atomic.__enter__ = __enter__
|
|
147
|
+
|
|
148
|
+
self.atomic_is_patched = True
|
|
149
|
+
logger.debug("django.db.transaction.Atomic.__enter__ has been patched")
|
|
150
|
+
|
|
151
|
+
def reset(self, connection_name: str = "default"):
|
|
152
|
+
if connection_name not in self.tokens:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
from django.db.backends.signals import connection_created
|
|
156
|
+
|
|
157
|
+
connection = get_connection(connection_name)
|
|
158
|
+
|
|
159
|
+
connection.execute_wrappers = [
|
|
160
|
+
w
|
|
161
|
+
for w in connection.execute_wrappers
|
|
162
|
+
if getattr(w, "__name__", None) != "set_token_wrapper"
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
dispatch_uid = f"dbtokenmanager:{id(self)}:{connection_name}"
|
|
166
|
+
connection_created.disconnect(dispatch_uid=dispatch_uid)
|
|
167
|
+
|
|
168
|
+
self.tokens.pop(connection_name, None)
|
|
169
|
+
|
|
170
|
+
if not self.tokens:
|
|
171
|
+
from django.db.transaction import Atomic
|
|
172
|
+
|
|
173
|
+
Atomic.__enter__ = self.original_atomic_enter
|
|
174
|
+
self.atomic_is_patched = False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
db_token_manager = DBTokenManager()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def close_if_health_check_failed(self) -> None:
|
|
181
|
+
if self.close_at is not None:
|
|
182
|
+
if time.monotonic() >= self.close_at:
|
|
183
|
+
self.close()
|
|
184
|
+
self.close_at = time.monotonic() + CONN_MAX_AGE
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# this bundles set up and migration management
|
|
188
|
+
def setup_django(
|
|
189
|
+
isettings: InstanceSettings,
|
|
190
|
+
deploy_migrations: bool = False,
|
|
191
|
+
create_migrations: bool = False,
|
|
192
|
+
configure_only: bool = False,
|
|
193
|
+
init: bool = False,
|
|
194
|
+
view_schema: bool = False,
|
|
195
|
+
appname_number: tuple[str, int] | None = None,
|
|
196
|
+
):
|
|
197
|
+
if IS_RUN_FROM_IPYTHON:
|
|
198
|
+
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
|
|
199
|
+
logger.debug("DJANGO_ALLOW_ASYNC_UNSAFE env variable has been set to 'true'")
|
|
200
|
+
|
|
201
|
+
import dj_database_url
|
|
202
|
+
import django
|
|
203
|
+
from django.apps import apps
|
|
204
|
+
from django.conf import settings
|
|
205
|
+
from django.core.management import call_command
|
|
206
|
+
|
|
207
|
+
# configuration
|
|
208
|
+
if not settings.configured:
|
|
209
|
+
instance_db = isettings.db
|
|
210
|
+
if isettings.dialect == "postgresql":
|
|
211
|
+
if os.getenv("LAMIN_DB_SSL_REQUIRE") == "false":
|
|
212
|
+
ssl_require = False
|
|
213
|
+
else:
|
|
214
|
+
ssl_require = not is_local_db_url(instance_db)
|
|
215
|
+
options = {
|
|
216
|
+
"connect_timeout": os.getenv("PGCONNECT_TIMEOUT", 20),
|
|
217
|
+
"gssencmode": "disable",
|
|
218
|
+
}
|
|
219
|
+
else:
|
|
220
|
+
ssl_require = False
|
|
221
|
+
options = {}
|
|
222
|
+
default_db = dj_database_url.config(
|
|
223
|
+
env="LAMINDB_DJANGO_DATABASE_URL",
|
|
224
|
+
default=instance_db,
|
|
225
|
+
# see comment next to patching BaseDatabaseWrapper below
|
|
226
|
+
conn_max_age=CONN_MAX_AGE,
|
|
227
|
+
conn_health_checks=True,
|
|
228
|
+
ssl_require=ssl_require,
|
|
229
|
+
)
|
|
230
|
+
if options:
|
|
231
|
+
# do not overwrite keys in options if set
|
|
232
|
+
default_db["OPTIONS"] = {**options, **default_db.get("OPTIONS", {})}
|
|
233
|
+
DATABASES = {
|
|
234
|
+
"default": default_db,
|
|
235
|
+
}
|
|
236
|
+
from .._init_instance import get_schema_module_name
|
|
237
|
+
|
|
238
|
+
module_names = ["core"] + list(isettings.modules)
|
|
239
|
+
raise_import_error = True if init else False
|
|
240
|
+
installed_apps = [
|
|
241
|
+
package_name
|
|
242
|
+
for name in module_names
|
|
243
|
+
if (
|
|
244
|
+
package_name := get_schema_module_name(
|
|
245
|
+
name, raise_import_error=raise_import_error
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
is not None
|
|
249
|
+
]
|
|
250
|
+
if view_schema:
|
|
251
|
+
installed_apps = installed_apps[::-1] # to fix how apps appear
|
|
252
|
+
installed_apps += ["schema_graph", "django.contrib.staticfiles"]
|
|
253
|
+
if isettings.dialect == "postgresql":
|
|
254
|
+
installed_apps.insert(0, "pgtrigger")
|
|
255
|
+
|
|
256
|
+
kwargs = dict(
|
|
257
|
+
INSTALLED_APPS=installed_apps,
|
|
258
|
+
DATABASES=DATABASES,
|
|
259
|
+
DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
|
|
260
|
+
TIME_ZONE="UTC",
|
|
261
|
+
USE_TZ=True,
|
|
262
|
+
)
|
|
263
|
+
if view_schema:
|
|
264
|
+
kwargs.update(
|
|
265
|
+
DEBUG=True,
|
|
266
|
+
ROOT_URLCONF="lamindb_setup._schema",
|
|
267
|
+
SECRET_KEY="dummy",
|
|
268
|
+
TEMPLATES=[
|
|
269
|
+
{
|
|
270
|
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
|
271
|
+
"APP_DIRS": True,
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
STATIC_ROOT=f"{Path.home().as_posix()}/.lamin/",
|
|
275
|
+
STATICFILES_FINDERS=[
|
|
276
|
+
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
|
277
|
+
],
|
|
278
|
+
STATIC_URL="static/",
|
|
279
|
+
)
|
|
280
|
+
if logger._verbosity == 5: # debug-level verbosity
|
|
281
|
+
kwargs.update(
|
|
282
|
+
{
|
|
283
|
+
"DEBUG": True,
|
|
284
|
+
"LOGGING": {
|
|
285
|
+
"version": 1,
|
|
286
|
+
"handlers": {
|
|
287
|
+
"console": {
|
|
288
|
+
"level": "DEBUG",
|
|
289
|
+
"class": "logging.StreamHandler",
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
"loggers": {
|
|
293
|
+
"django.db.backends": {
|
|
294
|
+
"level": "DEBUG",
|
|
295
|
+
"handlers": ["console"],
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
settings.configure(**kwargs)
|
|
302
|
+
# this isn't needed the first time django.setup() is called, but for unknown reason it's needed the second time
|
|
303
|
+
# the first time, it already defaults to true
|
|
304
|
+
apps.apps_ready = True
|
|
305
|
+
django.setup(set_prefix=False)
|
|
306
|
+
# https://laminlabs.slack.com/archives/C04FPE8V01W/p1698239551460289
|
|
307
|
+
from django.db.backends.base.base import BaseDatabaseWrapper
|
|
308
|
+
|
|
309
|
+
BaseDatabaseWrapper.close_if_health_check_failed = close_if_health_check_failed
|
|
310
|
+
logger.debug(
|
|
311
|
+
"django.db.backends.base.base.BaseDatabaseWrapper.close_if_health_check_failed has been patched"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
disable_context: bool = False
|
|
315
|
+
if (
|
|
316
|
+
env_disable_context := os.getenv("LAMINDB_DISABLE_CONNECTION_CONTEXT")
|
|
317
|
+
) is not None:
|
|
318
|
+
disable_context = env_disable_context == "true"
|
|
319
|
+
elif IS_RUN_FROM_IPYTHON:
|
|
320
|
+
from ipykernel import __version__ as ipykernel_version
|
|
321
|
+
|
|
322
|
+
disable_context = version.parse(ipykernel_version) >= version.parse("7.0.0")
|
|
323
|
+
if disable_context:
|
|
324
|
+
django.db.connections._connections = threading.local()
|
|
325
|
+
logger.debug("django.db.connections._connections has been patched")
|
|
326
|
+
|
|
327
|
+
# error if trying to query with the default connection without setting up an instance
|
|
328
|
+
get_connection("default").execute_wrappers.insert(0, error_no_instance_wrapper)
|
|
329
|
+
|
|
330
|
+
if isettings._fine_grained_access and isettings._db_permissions == "jwt":
|
|
331
|
+
db_token = DBToken(isettings)
|
|
332
|
+
db_token_manager.set(db_token) # sets for the default connection
|
|
333
|
+
|
|
334
|
+
if configure_only:
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
# migrations management
|
|
338
|
+
if create_migrations:
|
|
339
|
+
call_command("makemigrations")
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
if deploy_migrations:
|
|
343
|
+
if appname_number is None:
|
|
344
|
+
call_command("migrate", verbosity=2)
|
|
345
|
+
else:
|
|
346
|
+
app_name, app_number = appname_number
|
|
347
|
+
call_command("migrate", app_name, app_number, verbosity=2)
|
|
348
|
+
isettings._update_cloud_sqlite_file(unlock_cloud_sqlite=False)
|
|
349
|
+
elif init:
|
|
350
|
+
modules_beyond_bionty = isettings.modules.copy()
|
|
351
|
+
compressed_sqlite_path = Path(__file__).parent / "lamin.db.gz"
|
|
352
|
+
if "bionty" in modules_beyond_bionty:
|
|
353
|
+
modules_beyond_bionty.remove("bionty")
|
|
354
|
+
# seed from compressed sqlite file
|
|
355
|
+
if (
|
|
356
|
+
isettings.dialect == "sqlite"
|
|
357
|
+
and os.getenv("LAMINDB_INIT_FROM_SCRATCH", "false") != "true"
|
|
358
|
+
and len(modules_beyond_bionty) == 0
|
|
359
|
+
and compressed_sqlite_path.exists()
|
|
360
|
+
):
|
|
361
|
+
with gzip.open(compressed_sqlite_path, "rb") as f_in:
|
|
362
|
+
with open(isettings._sqlite_file_local, "wb") as f_out:
|
|
363
|
+
shutil.copyfileobj(f_in, f_out)
|
|
364
|
+
global IS_MIGRATING
|
|
365
|
+
IS_MIGRATING = True
|
|
366
|
+
call_command("migrate", verbosity=0)
|
|
367
|
+
IS_MIGRATING = False
|
|
368
|
+
|
|
369
|
+
global IS_SETUP
|
|
370
|
+
IS_SETUP = True
|
|
371
|
+
|
|
372
|
+
if isettings.keep_artifacts_local:
|
|
373
|
+
isettings._local_storage = isettings._search_local_root()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# these needs to be followed by
|
|
377
|
+
# setup_django()
|
|
378
|
+
# reset_django_module_variables()
|
|
379
|
+
def reset_django():
|
|
380
|
+
from django.conf import settings
|
|
381
|
+
from django.apps import apps
|
|
382
|
+
from django.db import connections
|
|
383
|
+
|
|
384
|
+
if not settings.configured:
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
connections.close_all()
|
|
388
|
+
|
|
389
|
+
global db_token_manager
|
|
390
|
+
|
|
391
|
+
db_token_manager.reset()
|
|
392
|
+
|
|
393
|
+
if getattr(settings, "_wrapped", None) is not None:
|
|
394
|
+
settings._wrapped = None
|
|
395
|
+
|
|
396
|
+
app_names = {"django"} | {app.name for app in apps.get_app_configs()}
|
|
397
|
+
|
|
398
|
+
apps.app_configs.clear()
|
|
399
|
+
apps.all_models.clear()
|
|
400
|
+
apps.apps_ready = apps.models_ready = apps.ready = apps.loading = False
|
|
401
|
+
apps.clear_cache()
|
|
402
|
+
|
|
403
|
+
# i suspect it is enough to just drop django and all the apps from sys.modules
|
|
404
|
+
# the code above is just a precaution
|
|
405
|
+
for module_name in list(sys.modules):
|
|
406
|
+
if module_name.partition(".")[0] in app_names:
|
|
407
|
+
del sys.modules[module_name]
|
|
408
|
+
|
|
409
|
+
il.invalidate_caches()
|
|
410
|
+
|
|
411
|
+
db_token_manager = DBTokenManager()
|
|
412
|
+
|
|
413
|
+
global IS_SETUP
|
|
414
|
+
IS_SETUP = False
|