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