lamindb_setup 1.9.0__py3-none-any.whl → 1.9.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 (39) hide show
  1. lamindb_setup/__init__.py +107 -107
  2. lamindb_setup/_cache.py +87 -87
  3. lamindb_setup/_check_setup.py +166 -166
  4. lamindb_setup/_connect_instance.py +328 -342
  5. lamindb_setup/_delete.py +141 -141
  6. lamindb_setup/_disconnect.py +32 -32
  7. lamindb_setup/_init_instance.py +440 -440
  8. lamindb_setup/_migrate.py +266 -266
  9. lamindb_setup/_register_instance.py +35 -35
  10. lamindb_setup/_schema_metadata.py +441 -441
  11. lamindb_setup/_set_managed_storage.py +70 -70
  12. lamindb_setup/_setup_user.py +133 -133
  13. lamindb_setup/core/__init__.py +21 -21
  14. lamindb_setup/core/_aws_options.py +223 -223
  15. lamindb_setup/core/_hub_client.py +248 -248
  16. lamindb_setup/core/_hub_core.py +665 -665
  17. lamindb_setup/core/_hub_crud.py +227 -227
  18. lamindb_setup/core/_private_django_api.py +83 -83
  19. lamindb_setup/core/_settings.py +377 -377
  20. lamindb_setup/core/_settings_instance.py +569 -569
  21. lamindb_setup/core/_settings_load.py +141 -141
  22. lamindb_setup/core/_settings_save.py +95 -95
  23. lamindb_setup/core/_settings_storage.py +429 -429
  24. lamindb_setup/core/_settings_store.py +91 -91
  25. lamindb_setup/core/_settings_user.py +55 -55
  26. lamindb_setup/core/_setup_bionty_sources.py +44 -44
  27. lamindb_setup/core/cloud_sqlite_locker.py +240 -240
  28. lamindb_setup/core/django.py +305 -296
  29. lamindb_setup/core/exceptions.py +1 -1
  30. lamindb_setup/core/hashing.py +134 -134
  31. lamindb_setup/core/types.py +1 -1
  32. lamindb_setup/core/upath.py +1013 -1013
  33. lamindb_setup/errors.py +70 -70
  34. lamindb_setup/types.py +20 -20
  35. {lamindb_setup-1.9.0.dist-info → lamindb_setup-1.9.1.dist-info}/METADATA +1 -1
  36. lamindb_setup-1.9.1.dist-info/RECORD +50 -0
  37. lamindb_setup-1.9.0.dist-info/RECORD +0 -50
  38. {lamindb_setup-1.9.0.dist-info → lamindb_setup-1.9.1.dist-info}/LICENSE +0 -0
  39. {lamindb_setup-1.9.0.dist-info → lamindb_setup-1.9.1.dist-info}/WHEEL +0 -0
@@ -1,296 +1,305 @@
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 jwt
9
- import time
10
- from pathlib import Path
11
- import time
12
- from ._settings_instance import InstanceSettings
13
-
14
- from lamin_utils import logger
15
-
16
-
17
- IS_RUN_FROM_IPYTHON = getattr(builtins, "__IPYTHON__", False)
18
- IS_SETUP = False
19
- IS_MIGRATING = False
20
- CONN_MAX_AGE = 299
21
-
22
-
23
- # db token that refreshes on access if needed
24
- class DBToken:
25
- def __init__(
26
- self, instance: InstanceSettings | dict, access_token: str | None = None
27
- ):
28
- self.instance = instance
29
- self.access_token = access_token
30
- # initialized in token_query
31
- self._token: str | None = None
32
- self._token_query: str | None = None
33
- self._expiration: float
34
-
35
- def _refresh_token(self):
36
- from ._hub_core import access_db
37
- from psycopg2.extensions import adapt
38
-
39
- self._token = access_db(self.instance, self.access_token)
40
- self._token_query = (
41
- f"SELECT set_token({adapt(self._token).getquoted().decode()}, true);"
42
- )
43
- self._expiration = jwt.decode(self._token, options={"verify_signature": False})[
44
- "exp"
45
- ]
46
-
47
- @property
48
- def token_query(self) -> str:
49
- # refresh token if needed
50
- if self._token is None or time.time() >= self._expiration:
51
- self._refresh_token()
52
-
53
- return self._token_query # type: ignore
54
-
55
-
56
- # a class to manage jwt in dbs
57
- class DBTokenManager:
58
- def __init__(self):
59
- from django.db.transaction import Atomic
60
-
61
- self.original_atomic_enter = Atomic.__enter__
62
-
63
- self.tokens: dict[str, DBToken] = {}
64
-
65
- def get_connection(self, connection_name: str):
66
- from django.db import connections
67
-
68
- connection = connections[connection_name]
69
- assert connection.vendor == "postgresql"
70
-
71
- return connection
72
-
73
- def set(self, token: DBToken, connection_name: str = "default"):
74
- from django.db.transaction import Atomic
75
-
76
- connection = self.get_connection(connection_name)
77
-
78
- def set_token_wrapper(execute, sql, params, many, context):
79
- not_in_atomic_block = (
80
- context is None
81
- or "connection" not in context
82
- or not context["connection"].in_atomic_block
83
- )
84
- # ignore atomic blocks
85
- if not_in_atomic_block:
86
- sql = token.token_query + sql
87
- result = execute(sql, params, many, context)
88
- # this ensures that psycopg3 in the current env doesn't break this wrapper
89
- # psycopg3 returns a cursor
90
- # psycopg3 fetching differs from psycopg2, it returns the output of all sql statements
91
- # not only the last one as psycopg2 does. So we shift the cursor from set_token
92
- if (
93
- not_in_atomic_block
94
- and result is not None
95
- and hasattr(result, "nextset")
96
- ):
97
- result.nextset()
98
- return result
99
-
100
- connection.execute_wrappers.append(set_token_wrapper)
101
-
102
- self.tokens[connection_name] = token
103
-
104
- # ensure we set the token only once for an outer atomic block
105
- def __enter__(atomic):
106
- self.original_atomic_enter(atomic)
107
- connection_name = "default" if atomic.using is None else atomic.using
108
- if connection_name in self.tokens:
109
- # here we don't use the connection from the closure
110
- # because Atomic is a single class to manage transactions for all connections
111
- connection = self.get_connection(connection_name)
112
- if len(connection.atomic_blocks) == 1:
113
- token = self.tokens[connection_name]
114
- # use raw psycopg2 connection here
115
- # atomic block ensures connection
116
- connection.connection.cursor().execute(token.token_query)
117
-
118
- Atomic.__enter__ = __enter__
119
- logger.debug("django.db.transaction.Atomic.__enter__ has been patched")
120
-
121
- def reset(self, connection_name: str = "default"):
122
- connection = self.get_connection(connection_name)
123
-
124
- connection.execute_wrappers = [
125
- w
126
- for w in connection.execute_wrappers
127
- if getattr(w, "__name__", None) != "set_token_wrapper"
128
- ]
129
-
130
- self.tokens.pop(connection_name, None)
131
-
132
-
133
- db_token_manager = DBTokenManager()
134
-
135
-
136
- def close_if_health_check_failed(self) -> None:
137
- if self.close_at is not None:
138
- if time.monotonic() >= self.close_at:
139
- self.close()
140
- self.close_at = time.monotonic() + CONN_MAX_AGE
141
-
142
-
143
- # this bundles set up and migration management
144
- def setup_django(
145
- isettings: InstanceSettings,
146
- deploy_migrations: bool = False,
147
- create_migrations: bool = False,
148
- configure_only: bool = False,
149
- init: bool = False,
150
- view_schema: bool = False,
151
- appname_number: tuple[str, int] | None = None,
152
- ):
153
- if IS_RUN_FROM_IPYTHON:
154
- os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
155
- logger.debug("DJANGO_ALLOW_ASYNC_UNSAFE env variable has been set to 'true'")
156
-
157
- import dj_database_url
158
- import django
159
- from django.conf import settings
160
- from django.core.management import call_command
161
-
162
- # configuration
163
- if not settings.configured:
164
- default_db = dj_database_url.config(
165
- env="LAMINDB_DJANGO_DATABASE_URL",
166
- default=isettings.db,
167
- # see comment next to patching BaseDatabaseWrapper below
168
- conn_max_age=CONN_MAX_AGE,
169
- conn_health_checks=True,
170
- )
171
- DATABASES = {
172
- "default": default_db,
173
- }
174
- from .._init_instance import get_schema_module_name
175
-
176
- module_names = ["core"] + list(isettings.modules)
177
- raise_import_error = True if init else False
178
- installed_apps = [
179
- package_name
180
- for name in module_names
181
- if (
182
- package_name := get_schema_module_name(
183
- name, raise_import_error=raise_import_error
184
- )
185
- )
186
- is not None
187
- ]
188
- if view_schema:
189
- installed_apps = installed_apps[::-1] # to fix how apps appear
190
- installed_apps += ["schema_graph", "django.contrib.staticfiles"]
191
-
192
- kwargs = dict(
193
- INSTALLED_APPS=installed_apps,
194
- DATABASES=DATABASES,
195
- DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
196
- TIME_ZONE="UTC",
197
- USE_TZ=True,
198
- )
199
- if view_schema:
200
- kwargs.update(
201
- DEBUG=True,
202
- ROOT_URLCONF="lamindb_setup._schema",
203
- SECRET_KEY="dummy",
204
- TEMPLATES=[
205
- {
206
- "BACKEND": "django.template.backends.django.DjangoTemplates",
207
- "APP_DIRS": True,
208
- },
209
- ],
210
- STATIC_ROOT=f"{Path.home().as_posix()}/.lamin/",
211
- STATICFILES_FINDERS=[
212
- "django.contrib.staticfiles.finders.AppDirectoriesFinder",
213
- ],
214
- STATIC_URL="static/",
215
- )
216
- settings.configure(**kwargs)
217
- django.setup(set_prefix=False)
218
- # https://laminlabs.slack.com/archives/C04FPE8V01W/p1698239551460289
219
- from django.db.backends.base.base import BaseDatabaseWrapper
220
-
221
- BaseDatabaseWrapper.close_if_health_check_failed = close_if_health_check_failed
222
- logger.debug(
223
- "django.db.backends.base.base.BaseDatabaseWrapper.close_if_health_check_failed has been patched"
224
- )
225
-
226
- if isettings._fine_grained_access and isettings._db_permissions == "jwt":
227
- db_token = DBToken(isettings)
228
- db_token_manager.set(db_token) # sets for the default connection
229
-
230
- if configure_only:
231
- return None
232
-
233
- # migrations management
234
- if create_migrations:
235
- call_command("makemigrations")
236
- return None
237
-
238
- if deploy_migrations:
239
- if appname_number is None:
240
- call_command("migrate", verbosity=2)
241
- else:
242
- app_name, app_number = appname_number
243
- call_command("migrate", app_name, app_number, verbosity=2)
244
- isettings._update_cloud_sqlite_file(unlock_cloud_sqlite=False)
245
- elif init:
246
- global IS_MIGRATING
247
- IS_MIGRATING = True
248
- call_command("migrate", verbosity=0)
249
- IS_MIGRATING = False
250
-
251
- global IS_SETUP
252
- IS_SETUP = True
253
-
254
- if isettings.keep_artifacts_local:
255
- isettings._local_storage = isettings._search_local_root()
256
-
257
-
258
- # THIS IS NOT SAFE
259
- # especially if lamindb is imported already
260
- # django.setup fails if called for the second time
261
- # reset_django() allows to call setup again,
262
- # needed to connect to a different instance in the same process if connected already
263
- # there could be problems if models are already imported from lamindb or other modules
264
- # these 'old' models can have any number of problems
265
- def reset_django():
266
- from django.conf import settings
267
- from django.apps import apps
268
- from django.db import connections
269
-
270
- if not settings.configured:
271
- return
272
-
273
- connections.close_all()
274
-
275
- if getattr(settings, "_wrapped", None) is not None:
276
- settings._wrapped = None
277
-
278
- app_names = {"django"} | {app.name for app in apps.get_app_configs()}
279
-
280
- apps.app_configs.clear()
281
- apps.apps_ready = apps.models_ready = apps.ready = apps.loading = False
282
- apps.clear_cache()
283
-
284
- # i suspect it is enough to just drop django and all the apps from sys.modules
285
- # the code above is just a precaution
286
- for module_name in list(sys.modules):
287
- if module_name.partition(".")[0] in app_names:
288
- del sys.modules[module_name]
289
-
290
- il.invalidate_caches()
291
-
292
- global db_token_manager
293
- db_token_manager = DBTokenManager()
294
-
295
- global IS_SETUP
296
- 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 jwt
9
+ import time
10
+ from pathlib import Path
11
+ import time
12
+ from ._settings_instance import InstanceSettings, is_local_db_url
13
+
14
+ from lamin_utils import logger
15
+
16
+
17
+ IS_RUN_FROM_IPYTHON = getattr(builtins, "__IPYTHON__", False)
18
+ IS_SETUP = False
19
+ IS_MIGRATING = False
20
+ CONN_MAX_AGE = 299
21
+
22
+
23
+ # db token that refreshes on access if needed
24
+ class DBToken:
25
+ def __init__(
26
+ self, instance: InstanceSettings | dict, access_token: str | None = None
27
+ ):
28
+ self.instance = instance
29
+ self.access_token = access_token
30
+ # initialized in token_query
31
+ self._token: str | None = None
32
+ self._token_query: str | None = None
33
+ self._expiration: float
34
+
35
+ def _refresh_token(self):
36
+ from ._hub_core import access_db
37
+ from psycopg2.extensions import adapt
38
+
39
+ self._token = access_db(self.instance, self.access_token)
40
+ self._token_query = (
41
+ f"SELECT set_token({adapt(self._token).getquoted().decode()}, true);"
42
+ )
43
+ self._expiration = jwt.decode(self._token, options={"verify_signature": False})[
44
+ "exp"
45
+ ]
46
+
47
+ @property
48
+ def token_query(self) -> str:
49
+ # refresh token if needed
50
+ if self._token is None or time.time() >= self._expiration:
51
+ self._refresh_token()
52
+
53
+ return self._token_query # type: ignore
54
+
55
+
56
+ # a class to manage jwt in dbs
57
+ class DBTokenManager:
58
+ def __init__(self):
59
+ from django.db.transaction import Atomic
60
+
61
+ self.original_atomic_enter = Atomic.__enter__
62
+
63
+ self.tokens: dict[str, DBToken] = {}
64
+
65
+ def get_connection(self, connection_name: str):
66
+ from django.db import connections
67
+
68
+ connection = connections[connection_name]
69
+ assert connection.vendor == "postgresql"
70
+
71
+ return connection
72
+
73
+ def set(self, token: DBToken, connection_name: str = "default"):
74
+ from django.db.transaction import Atomic
75
+
76
+ connection = self.get_connection(connection_name)
77
+
78
+ def set_token_wrapper(execute, sql, params, many, context):
79
+ not_in_atomic_block = (
80
+ context is None
81
+ or "connection" not in context
82
+ or not context["connection"].in_atomic_block
83
+ )
84
+ # ignore atomic blocks
85
+ if not_in_atomic_block:
86
+ sql = token.token_query + sql
87
+ result = execute(sql, params, many, context)
88
+ # this ensures that psycopg3 in the current env doesn't break this wrapper
89
+ # psycopg3 returns a cursor
90
+ # psycopg3 fetching differs from psycopg2, it returns the output of all sql statements
91
+ # not only the last one as psycopg2 does. So we shift the cursor from set_token
92
+ if (
93
+ not_in_atomic_block
94
+ and result is not None
95
+ and hasattr(result, "nextset")
96
+ ):
97
+ result.nextset()
98
+ return result
99
+
100
+ connection.execute_wrappers.append(set_token_wrapper)
101
+
102
+ self.tokens[connection_name] = token
103
+
104
+ # ensure we set the token only once for an outer atomic block
105
+ def __enter__(atomic):
106
+ self.original_atomic_enter(atomic)
107
+ connection_name = "default" if atomic.using is None else atomic.using
108
+ if connection_name in self.tokens:
109
+ # here we don't use the connection from the closure
110
+ # because Atomic is a single class to manage transactions for all connections
111
+ connection = self.get_connection(connection_name)
112
+ if len(connection.atomic_blocks) == 1:
113
+ token = self.tokens[connection_name]
114
+ # use raw psycopg2 connection here
115
+ # atomic block ensures connection
116
+ connection.connection.cursor().execute(token.token_query)
117
+
118
+ Atomic.__enter__ = __enter__
119
+ logger.debug("django.db.transaction.Atomic.__enter__ has been patched")
120
+
121
+ def reset(self, connection_name: str = "default"):
122
+ connection = self.get_connection(connection_name)
123
+
124
+ connection.execute_wrappers = [
125
+ w
126
+ for w in connection.execute_wrappers
127
+ if getattr(w, "__name__", None) != "set_token_wrapper"
128
+ ]
129
+
130
+ self.tokens.pop(connection_name, None)
131
+
132
+
133
+ db_token_manager = DBTokenManager()
134
+
135
+
136
+ def close_if_health_check_failed(self) -> None:
137
+ if self.close_at is not None:
138
+ if time.monotonic() >= self.close_at:
139
+ self.close()
140
+ self.close_at = time.monotonic() + CONN_MAX_AGE
141
+
142
+
143
+ # this bundles set up and migration management
144
+ def setup_django(
145
+ isettings: InstanceSettings,
146
+ deploy_migrations: bool = False,
147
+ create_migrations: bool = False,
148
+ configure_only: bool = False,
149
+ init: bool = False,
150
+ view_schema: bool = False,
151
+ appname_number: tuple[str, int] | None = None,
152
+ ):
153
+ if IS_RUN_FROM_IPYTHON:
154
+ os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
155
+ logger.debug("DJANGO_ALLOW_ASYNC_UNSAFE env variable has been set to 'true'")
156
+
157
+ import dj_database_url
158
+ import django
159
+ from django.conf import settings
160
+ from django.core.management import call_command
161
+
162
+ # configuration
163
+ if not settings.configured:
164
+ instance_db = isettings.db
165
+ if isettings.dialect == "postgresql":
166
+ if os.getenv("LAMIN_DB_SSL_REQUIRE") == "false":
167
+ ssl_require = False
168
+ else:
169
+ ssl_require = not is_local_db_url(instance_db)
170
+ else:
171
+ ssl_require = False
172
+ default_db = dj_database_url.config(
173
+ env="LAMINDB_DJANGO_DATABASE_URL",
174
+ default=instance_db,
175
+ # see comment next to patching BaseDatabaseWrapper below
176
+ conn_max_age=CONN_MAX_AGE,
177
+ conn_health_checks=True,
178
+ ssl_require=ssl_require,
179
+ )
180
+ DATABASES = {
181
+ "default": default_db,
182
+ }
183
+ from .._init_instance import get_schema_module_name
184
+
185
+ module_names = ["core"] + list(isettings.modules)
186
+ raise_import_error = True if init else False
187
+ installed_apps = [
188
+ package_name
189
+ for name in module_names
190
+ if (
191
+ package_name := get_schema_module_name(
192
+ name, raise_import_error=raise_import_error
193
+ )
194
+ )
195
+ is not None
196
+ ]
197
+ if view_schema:
198
+ installed_apps = installed_apps[::-1] # to fix how apps appear
199
+ installed_apps += ["schema_graph", "django.contrib.staticfiles"]
200
+
201
+ kwargs = dict(
202
+ INSTALLED_APPS=installed_apps,
203
+ DATABASES=DATABASES,
204
+ DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
205
+ TIME_ZONE="UTC",
206
+ USE_TZ=True,
207
+ )
208
+ if view_schema:
209
+ kwargs.update(
210
+ DEBUG=True,
211
+ ROOT_URLCONF="lamindb_setup._schema",
212
+ SECRET_KEY="dummy",
213
+ TEMPLATES=[
214
+ {
215
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
216
+ "APP_DIRS": True,
217
+ },
218
+ ],
219
+ STATIC_ROOT=f"{Path.home().as_posix()}/.lamin/",
220
+ STATICFILES_FINDERS=[
221
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
222
+ ],
223
+ STATIC_URL="static/",
224
+ )
225
+ settings.configure(**kwargs)
226
+ django.setup(set_prefix=False)
227
+ # https://laminlabs.slack.com/archives/C04FPE8V01W/p1698239551460289
228
+ from django.db.backends.base.base import BaseDatabaseWrapper
229
+
230
+ BaseDatabaseWrapper.close_if_health_check_failed = close_if_health_check_failed
231
+ logger.debug(
232
+ "django.db.backends.base.base.BaseDatabaseWrapper.close_if_health_check_failed has been patched"
233
+ )
234
+
235
+ if isettings._fine_grained_access and isettings._db_permissions == "jwt":
236
+ db_token = DBToken(isettings)
237
+ db_token_manager.set(db_token) # sets for the default connection
238
+
239
+ if configure_only:
240
+ return None
241
+
242
+ # migrations management
243
+ if create_migrations:
244
+ call_command("makemigrations")
245
+ return None
246
+
247
+ if deploy_migrations:
248
+ if appname_number is None:
249
+ call_command("migrate", verbosity=2)
250
+ else:
251
+ app_name, app_number = appname_number
252
+ call_command("migrate", app_name, app_number, verbosity=2)
253
+ isettings._update_cloud_sqlite_file(unlock_cloud_sqlite=False)
254
+ elif init:
255
+ global IS_MIGRATING
256
+ IS_MIGRATING = True
257
+ call_command("migrate", verbosity=0)
258
+ IS_MIGRATING = False
259
+
260
+ global IS_SETUP
261
+ IS_SETUP = True
262
+
263
+ if isettings.keep_artifacts_local:
264
+ isettings._local_storage = isettings._search_local_root()
265
+
266
+
267
+ # THIS IS NOT SAFE
268
+ # especially if lamindb is imported already
269
+ # django.setup fails if called for the second time
270
+ # reset_django() allows to call setup again,
271
+ # needed to connect to a different instance in the same process if connected already
272
+ # there could be problems if models are already imported from lamindb or other modules
273
+ # these 'old' models can have any number of problems
274
+ def reset_django():
275
+ from django.conf import settings
276
+ from django.apps import apps
277
+ from django.db import connections
278
+
279
+ if not settings.configured:
280
+ return
281
+
282
+ connections.close_all()
283
+
284
+ if getattr(settings, "_wrapped", None) is not None:
285
+ settings._wrapped = None
286
+
287
+ app_names = {"django"} | {app.name for app in apps.get_app_configs()}
288
+
289
+ apps.app_configs.clear()
290
+ apps.apps_ready = apps.models_ready = apps.ready = apps.loading = False
291
+ apps.clear_cache()
292
+
293
+ # i suspect it is enough to just drop django and all the apps from sys.modules
294
+ # the code above is just a precaution
295
+ for module_name in list(sys.modules):
296
+ if module_name.partition(".")[0] in app_names:
297
+ del sys.modules[module_name]
298
+
299
+ il.invalidate_caches()
300
+
301
+ global db_token_manager
302
+ db_token_manager = DBTokenManager()
303
+
304
+ global IS_SETUP
305
+ IS_SETUP = False
@@ -1 +1 @@
1
- from lamindb_setup.errors import DefaultMessageException # backwards compatibility
1
+ from lamindb_setup.errors import DefaultMessageException # backwards compatibility