lamindb_setup 0.78.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. lamindb_setup/__init__.py +74 -0
  2. lamindb_setup/_cache.py +48 -0
  3. lamindb_setup/_check.py +7 -0
  4. lamindb_setup/_check_setup.py +92 -0
  5. lamindb_setup/_close.py +35 -0
  6. lamindb_setup/_connect_instance.py +429 -0
  7. lamindb_setup/_delete.py +141 -0
  8. lamindb_setup/_django.py +39 -0
  9. lamindb_setup/_entry_points.py +22 -0
  10. lamindb_setup/_exportdb.py +68 -0
  11. lamindb_setup/_importdb.py +50 -0
  12. lamindb_setup/_init_instance.py +411 -0
  13. lamindb_setup/_migrate.py +239 -0
  14. lamindb_setup/_register_instance.py +36 -0
  15. lamindb_setup/_schema.py +27 -0
  16. lamindb_setup/_schema_metadata.py +411 -0
  17. lamindb_setup/_set_managed_storage.py +55 -0
  18. lamindb_setup/_setup_user.py +137 -0
  19. lamindb_setup/_silence_loggers.py +44 -0
  20. lamindb_setup/core/__init__.py +21 -0
  21. lamindb_setup/core/_aws_credentials.py +151 -0
  22. lamindb_setup/core/_aws_storage.py +48 -0
  23. lamindb_setup/core/_deprecated.py +55 -0
  24. lamindb_setup/core/_docs.py +14 -0
  25. lamindb_setup/core/_hub_client.py +173 -0
  26. lamindb_setup/core/_hub_core.py +554 -0
  27. lamindb_setup/core/_hub_crud.py +211 -0
  28. lamindb_setup/core/_hub_utils.py +109 -0
  29. lamindb_setup/core/_private_django_api.py +88 -0
  30. lamindb_setup/core/_settings.py +184 -0
  31. lamindb_setup/core/_settings_instance.py +485 -0
  32. lamindb_setup/core/_settings_load.py +117 -0
  33. lamindb_setup/core/_settings_save.py +92 -0
  34. lamindb_setup/core/_settings_storage.py +350 -0
  35. lamindb_setup/core/_settings_store.py +75 -0
  36. lamindb_setup/core/_settings_user.py +55 -0
  37. lamindb_setup/core/_setup_bionty_sources.py +101 -0
  38. lamindb_setup/core/cloud_sqlite_locker.py +237 -0
  39. lamindb_setup/core/django.py +115 -0
  40. lamindb_setup/core/exceptions.py +10 -0
  41. lamindb_setup/core/hashing.py +116 -0
  42. lamindb_setup/core/types.py +17 -0
  43. lamindb_setup/core/upath.py +779 -0
  44. lamindb_setup-0.78.0.dist-info/LICENSE +201 -0
  45. lamindb_setup-0.78.0.dist-info/METADATA +47 -0
  46. lamindb_setup-0.78.0.dist-info/RECORD +47 -0
  47. lamindb_setup-0.78.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,411 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import os
5
+ import sys
6
+ import uuid
7
+ from typing import TYPE_CHECKING, Literal
8
+ from uuid import UUID
9
+
10
+ from django.core.exceptions import FieldError
11
+ from django.db.utils import OperationalError, ProgrammingError
12
+ from lamin_utils import logger
13
+
14
+ from ._close import close as close_instance
15
+ from ._silence_loggers import silence_loggers
16
+ from .core import InstanceSettings
17
+ from .core._settings import settings
18
+ from .core._settings_instance import is_local_db_url
19
+ from .core._settings_storage import StorageSettings, init_storage
20
+ from .core.upath import UPath
21
+
22
+ if TYPE_CHECKING:
23
+ from pydantic import PostgresDsn
24
+
25
+ from .core._settings_user import UserSettings
26
+ from .core.types import UPathStr
27
+
28
+
29
+ def get_schema_module_name(schema_name) -> str:
30
+ import importlib.util
31
+
32
+ name_attempts = [f"lnschema_{schema_name.replace('-', '_')}", schema_name]
33
+ for name in name_attempts:
34
+ module_spec = importlib.util.find_spec(name)
35
+ if module_spec is not None:
36
+ return name
37
+ raise ImportError(
38
+ f"Python package for '{schema_name}' is not installed.\nIf your package is on PyPI, run `pip install {schema_name}`"
39
+ )
40
+
41
+
42
+ def register_storage_in_instance(ssettings: StorageSettings):
43
+ from lnschema_core.models import Storage
44
+ from lnschema_core.users import current_user_id
45
+
46
+ from .core.hashing import hash_and_encode_as_b62
47
+
48
+ if ssettings._instance_id is not None:
49
+ instance_uid = hash_and_encode_as_b62(ssettings._instance_id.hex)[:12]
50
+ else:
51
+ instance_uid = None
52
+ # how do we ensure that this function is only called passing
53
+ # the managing instance?
54
+ defaults = {
55
+ "root": ssettings.root_as_str,
56
+ "type": ssettings.type,
57
+ "region": ssettings.region,
58
+ "instance_uid": instance_uid,
59
+ "created_by_id": current_user_id(),
60
+ "run": None,
61
+ }
62
+ if ssettings._uid is not None:
63
+ defaults["uid"] = ssettings._uid
64
+ storage, _ = Storage.objects.update_or_create(
65
+ root=ssettings.root_as_str,
66
+ defaults=defaults,
67
+ )
68
+ return storage
69
+
70
+
71
+ def register_user(usettings):
72
+ from lnschema_core.models import User
73
+
74
+ try:
75
+ # need to have try except because of integer primary key migration
76
+ user, created = User.objects.update_or_create(
77
+ uid=usettings.uid,
78
+ defaults={
79
+ "handle": usettings.handle,
80
+ "name": usettings.name,
81
+ },
82
+ )
83
+ # for users with only read access, except via ProgrammingError
84
+ # ProgrammingError: permission denied for table lnschema_core_user
85
+ except (OperationalError, FieldError, ProgrammingError):
86
+ pass
87
+
88
+
89
+ def register_user_and_storage_in_instance(isettings: InstanceSettings, usettings):
90
+ """Register user & storage in DB."""
91
+ from django.db.utils import OperationalError
92
+
93
+ try:
94
+ register_user(usettings)
95
+ register_storage_in_instance(isettings.storage)
96
+ except OperationalError as error:
97
+ logger.warning(f"instance seems not set up ({error})")
98
+
99
+
100
+ def reload_schema_modules(isettings: InstanceSettings, include_core: bool = True):
101
+ schema_names = ["core"] if include_core else []
102
+ # schema_names += list(isettings.schema)
103
+ schema_module_names = [get_schema_module_name(n) for n in schema_names]
104
+
105
+ for schema_module_name in schema_module_names:
106
+ if schema_module_name in sys.modules:
107
+ schema_module = importlib.import_module(schema_module_name)
108
+ importlib.reload(schema_module)
109
+
110
+
111
+ def reload_lamindb_itself(isettings) -> bool:
112
+ reloaded = False
113
+ if "lamindb" in sys.modules:
114
+ import lamindb
115
+
116
+ importlib.reload(lamindb)
117
+ reloaded = True
118
+ return reloaded
119
+
120
+
121
+ def reload_lamindb(isettings: InstanceSettings):
122
+ log_message = settings.auto_connect
123
+ if not reload_lamindb_itself(isettings):
124
+ log_message = True
125
+ if log_message:
126
+ logger.important(f"connected lamindb: {isettings.slug}")
127
+
128
+
129
+ ERROR_SQLITE_CACHE = """
130
+ Your cached local SQLite file exists, while your cloud SQLite file ({}) doesn't.
131
+ Either delete your cache ({}) or add it back to the cloud (if delete was accidental).
132
+ """
133
+
134
+
135
+ def process_connect_response(
136
+ response: tuple | str, instance_identifier: str
137
+ ) -> tuple[
138
+ UUID,
139
+ Literal[
140
+ "instance-corrupted-or-deleted", "account-not-exists", "instance-not-found"
141
+ ],
142
+ ]:
143
+ # for internal use when creating instances through CICD
144
+ if isinstance(response, tuple) and response[0] == "instance-corrupted-or-deleted":
145
+ hub_result = response[1]
146
+ instance_state = response[0]
147
+ instance_id = UUID(hub_result["id"])
148
+ else:
149
+ instance_id_str = os.getenv("LAMINDB_INSTANCE_ID_INIT")
150
+ if instance_id_str is None:
151
+ instance_id = uuid.uuid5(uuid.NAMESPACE_URL, instance_identifier)
152
+ else:
153
+ instance_id = UUID(instance_id_str)
154
+ instance_state = response
155
+ return instance_id, instance_state
156
+
157
+
158
+ def validate_init_args(
159
+ *,
160
+ storage: UPathStr,
161
+ name: str | None = None,
162
+ db: PostgresDsn | None = None,
163
+ schema: str | None = None,
164
+ _test: bool = False,
165
+ _write_settings: bool = True,
166
+ _user: UserSettings | None = None,
167
+ ) -> tuple[
168
+ str,
169
+ UUID | None,
170
+ Literal[
171
+ "connected",
172
+ "instance-corrupted-or-deleted",
173
+ "account-not-exists",
174
+ "instance-not-found",
175
+ ],
176
+ str,
177
+ ]:
178
+ from ._connect_instance import connect
179
+ from .core._hub_utils import (
180
+ validate_schema_arg,
181
+ )
182
+
183
+ # should be called as the first thing
184
+ name_str = infer_instance_name(storage=storage, name=name, db=db)
185
+ owner_str = settings.user.handle if _user is None else _user.handle
186
+ # test whether instance exists by trying to load it
187
+ instance_slug = f"{owner_str}/{name_str}"
188
+ response = connect(
189
+ instance_slug,
190
+ _db=db,
191
+ _raise_not_found_error=False,
192
+ _test=_test,
193
+ _write_settings=_write_settings,
194
+ _user=_user,
195
+ )
196
+ instance_state: Literal[
197
+ "connected",
198
+ "instance-corrupted-or-deleted",
199
+ "account-not-exists",
200
+ "instance-not-found",
201
+ ] = "connected"
202
+ instance_id = None
203
+ if response is not None:
204
+ instance_id, instance_state = process_connect_response(response, instance_slug)
205
+ schema = validate_schema_arg(schema)
206
+ return name_str, instance_id, instance_state, instance_slug
207
+
208
+
209
+ MESSAGE_NO_MULTIPLE_INSTANCE = """
210
+ Currently don't support subsequent connection to different databases in the same
211
+ Python session.\n
212
+ Try running on the CLI: lamin settings set auto-connect false
213
+ """
214
+
215
+
216
+ def init(
217
+ *,
218
+ storage: UPathStr,
219
+ name: str | None = None,
220
+ db: PostgresDsn | None = None,
221
+ schema: str | None = None,
222
+ **kwargs,
223
+ ) -> None:
224
+ """Create and load a LaminDB instance.
225
+
226
+ Args:
227
+ storage: Either ``"create-s3"``, local or
228
+ remote folder (``"s3://..."`` or ``"gs://..."``).
229
+ name: Instance name.
230
+ db: Database connection url, do not pass for SQLite.
231
+ schema: Comma-separated string of schema modules. None if not set.
232
+ """
233
+ isettings = None
234
+ ssettings = None
235
+
236
+ _write_settings: bool = kwargs.get("_write_settings", True)
237
+ _test: bool = kwargs.get("_test", False)
238
+
239
+ # use this user instead of settings.user
240
+ # contains access_token
241
+ _user: UserSettings | None = kwargs.get("_user", None)
242
+ user_handle: str = settings.user.handle if _user is None else _user.handle
243
+ user__uuid: UUID = settings.user._uuid if _user is None else _user._uuid # type: ignore
244
+ access_token: str | None = None if _user is None else _user.access_token
245
+
246
+ try:
247
+ silence_loggers()
248
+ from ._check_setup import _check_instance_setup
249
+
250
+ if _check_instance_setup() and not _test:
251
+ raise RuntimeError(MESSAGE_NO_MULTIPLE_INSTANCE)
252
+ elif _write_settings:
253
+ close_instance(mute=True)
254
+ from .core._hub_core import init_instance as init_instance_hub
255
+
256
+ name_str, instance_id, instance_state, _ = validate_init_args(
257
+ storage=storage,
258
+ name=name,
259
+ db=db,
260
+ schema=schema,
261
+ _test=_test,
262
+ _write_settings=_write_settings,
263
+ _user=_user, # will get from settings.user if _user is None
264
+ )
265
+ if instance_state == "connected":
266
+ if _write_settings:
267
+ settings.auto_connect = True # we can also debate this switch here
268
+ return None
269
+ # the conditions here match `isettings.is_remote`, but I currently don't
270
+ # see a way of making this more elegant; should become possible if we
271
+ # remove the instance.storage_id FK on the hub
272
+ prevent_register_hub = is_local_db_url(db) if db is not None else False
273
+ ssettings, _ = init_storage(
274
+ storage,
275
+ instance_id=instance_id,
276
+ init_instance=True,
277
+ prevent_register_hub=prevent_register_hub,
278
+ created_by=user__uuid,
279
+ access_token=access_token,
280
+ )
281
+ isettings = InstanceSettings(
282
+ id=instance_id, # type: ignore
283
+ owner=user_handle,
284
+ name=name_str,
285
+ storage=ssettings,
286
+ db=db,
287
+ schema=schema,
288
+ uid=ssettings.uid,
289
+ # to lock passed user in isettings._cloud_sqlite_locker.lock()
290
+ _locker_user=_user, # only has effect if cloud sqlite
291
+ )
292
+ register_on_hub = (
293
+ isettings.is_remote and instance_state != "instance-corrupted-or-deleted"
294
+ )
295
+ if register_on_hub:
296
+ init_instance_hub(
297
+ isettings, account_id=user__uuid, access_token=access_token
298
+ )
299
+ validate_sqlite_state(isettings)
300
+ # why call it here if it is also called in load_from_isettings?
301
+ isettings._persist(write_to_disk=_write_settings)
302
+ if _test:
303
+ return None
304
+ isettings._init_db()
305
+ load_from_isettings(
306
+ isettings, init=True, user=_user, write_settings=_write_settings
307
+ )
308
+ if _write_settings and isettings._is_cloud_sqlite:
309
+ isettings._cloud_sqlite_locker.lock()
310
+ logger.warning(
311
+ "locked instance (to unlock and push changes to the cloud SQLite file,"
312
+ " call: lamin disconnect)"
313
+ )
314
+ if register_on_hub and isettings.dialect != "sqlite":
315
+ from ._schema_metadata import update_schema_in_hub
316
+
317
+ update_schema_in_hub(access_token=access_token)
318
+ if _write_settings:
319
+ settings.auto_connect = True
320
+ except Exception as e:
321
+ from ._delete import delete_by_isettings
322
+ from .core._hub_core import delete_instance_record, delete_storage_record
323
+
324
+ if isettings is not None:
325
+ if _write_settings:
326
+ delete_by_isettings(isettings)
327
+ else:
328
+ settings._instance_settings = None
329
+ if (
330
+ user_handle != "anonymous" or access_token is not None
331
+ ) and isettings.is_on_hub:
332
+ delete_instance_record(isettings._id, access_token=access_token)
333
+ if (
334
+ ssettings is not None
335
+ and (user_handle != "anonymous" or access_token is not None)
336
+ and ssettings.is_on_hub
337
+ ):
338
+ delete_storage_record(ssettings._uuid, access_token=access_token) # type: ignore
339
+ raise e
340
+ return None
341
+
342
+
343
+ def load_from_isettings(
344
+ isettings: InstanceSettings,
345
+ *,
346
+ init: bool = False,
347
+ user: UserSettings | None = None,
348
+ write_settings: bool = True,
349
+ ) -> None:
350
+ from .core._setup_bionty_sources import write_bionty_sources
351
+
352
+ user = settings.user if user is None else user
353
+
354
+ if init:
355
+ # during init both user and storage need to be registered
356
+ register_user_and_storage_in_instance(isettings, user)
357
+ write_bionty_sources(isettings)
358
+ isettings._update_cloud_sqlite_file(unlock_cloud_sqlite=False)
359
+ else:
360
+ # when loading, django is already set up
361
+ #
362
+ # only register user if the instance is connected
363
+ # for the first time in an environment
364
+ # this is our best proxy for that the user might not
365
+ # yet be registered
366
+ if not isettings._get_settings_file().exists():
367
+ register_user(user)
368
+ isettings._persist(write_to_disk=write_settings)
369
+ reload_lamindb(isettings)
370
+
371
+
372
+ def validate_sqlite_state(isettings: InstanceSettings) -> None:
373
+ if isettings._is_cloud_sqlite:
374
+ if (
375
+ # it's important to first evaluate the existence check
376
+ # for the local sqlite file because it doesn't need a network
377
+ # request
378
+ isettings._sqlite_file_local.exists()
379
+ and not isettings._sqlite_file.exists()
380
+ ):
381
+ raise RuntimeError(
382
+ ERROR_SQLITE_CACHE.format(
383
+ isettings._sqlite_file, isettings._sqlite_file_local
384
+ )
385
+ )
386
+
387
+
388
+ def infer_instance_name(
389
+ *,
390
+ storage: UPathStr,
391
+ name: str | None = None,
392
+ db: PostgresDsn | None = None,
393
+ ) -> str:
394
+ if name is not None:
395
+ if "/" in name:
396
+ raise ValueError("Invalid instance name: '/' delimiter not allowed.")
397
+ return name
398
+ if db is not None:
399
+ logger.warning("using the sql database name for the instance name")
400
+ # this isn't a great way to access the db name
401
+ # could use LaminDsn instead
402
+ return str(db).split("/")[-1]
403
+ if storage == "create-s3":
404
+ raise ValueError("pass name to init if storage = 'create-s3'")
405
+ storage_path = UPath(storage)
406
+ if storage_path.name != "":
407
+ name = storage_path.name
408
+ else:
409
+ # dedicated treatment of bucket names
410
+ name = storage_path._url.netloc
411
+ return name.lower()
@@ -0,0 +1,239 @@
1
+ from __future__ import annotations
2
+
3
+ from django.db import connection
4
+ from django.db.migrations.loader import MigrationLoader
5
+ from lamin_utils import logger
6
+ from packaging import version
7
+
8
+ from ._check_setup import _check_instance_setup
9
+ from .core._settings import settings
10
+ from .core.django import setup_django
11
+
12
+
13
+ # for the django-based synching code, see laminhub_rest
14
+ def check_whether_migrations_in_sync(db_version_str: str):
15
+ from importlib import metadata
16
+
17
+ try:
18
+ installed_version_str = metadata.version("lamindb")
19
+ except metadata.PackageNotFoundError:
20
+ return None
21
+ if db_version_str is None:
22
+ logger.warning("no lamindb version stored to compare with installed version")
23
+ return None
24
+ installed_version = version.parse(installed_version_str)
25
+ db_version = version.parse(db_version_str)
26
+ if (
27
+ installed_version.major < db_version.major
28
+ or installed_version.minor < db_version.minor
29
+ ):
30
+ db_version_lower = f"{db_version.major}.{db_version.minor}"
31
+ db_version_upper = f"{db_version.major}.{db_version.minor + 1}"
32
+ logger.warning(
33
+ f"the database ({db_version_str}) is ahead of your installed lamindb"
34
+ f" package ({installed_version_str})"
35
+ )
36
+ logger.important(
37
+ "please update lamindb: pip install"
38
+ f' "lamindb>={db_version_lower},<{db_version_upper}"'
39
+ )
40
+ elif (
41
+ installed_version.major > db_version.major
42
+ or installed_version.minor > db_version.minor
43
+ ):
44
+ logger.warning(
45
+ f"the database ({db_version_str}) is behind your installed lamindb package"
46
+ f" ({installed_version_str})"
47
+ )
48
+ logger.important("consider migrating your database: lamin migrate deploy")
49
+
50
+
51
+ # for tests, see lamin-cli
52
+ class migrate:
53
+ """Manage migrations.
54
+
55
+ Examples:
56
+
57
+ >>> import lamindb as ln
58
+ >>> ln.setup.migrate.create()
59
+ >>> ln.setup.migrate.deploy()
60
+ >>> ln.setup.migrate.check()
61
+
62
+ """
63
+
64
+ @classmethod
65
+ def create(cls) -> None:
66
+ """Create a migration."""
67
+ if _check_instance_setup():
68
+ raise RuntimeError("Restart Python session to create migration or use CLI!")
69
+ setup_django(settings.instance, create_migrations=True)
70
+
71
+ @classmethod
72
+ def deploy(cls) -> None:
73
+ """Deploy a migration."""
74
+ from ._schema_metadata import update_schema_in_hub
75
+
76
+ if _check_instance_setup():
77
+ raise RuntimeError("Restart Python session to migrate or use CLI!")
78
+ from lamindb_setup.core._hub_client import call_with_fallback_auth
79
+ from lamindb_setup.core._hub_crud import (
80
+ select_collaborator,
81
+ update_instance,
82
+ )
83
+
84
+ if settings.instance.is_on_hub:
85
+ # double check that user is an admin, otherwise will fail below
86
+ # due to insufficient SQL permissions with cryptic error
87
+ collaborator = call_with_fallback_auth(
88
+ select_collaborator,
89
+ instance_id=settings.instance._id,
90
+ account_id=settings.user._uuid,
91
+ )
92
+ if collaborator is None or collaborator["role"] != "admin":
93
+ raise SystemExit(
94
+ "❌ Only admins can deploy migrations, please ensure that you're an"
95
+ f" admin: https://lamin.ai/{settings.instance.slug}/settings"
96
+ )
97
+ # we need lamindb to be installed, otherwise we can't populate the version
98
+ # information in the hub
99
+ import lamindb
100
+
101
+ # this sets up django and deploys the migrations
102
+ setup_django(settings.instance, deploy_migrations=True)
103
+ # this populates the hub
104
+ if settings.instance.is_on_hub:
105
+ logger.important(f"updating lamindb version in hub: {lamindb.__version__}")
106
+ # TODO: integrate update of instance table within update_schema_in_hub & below
107
+ if settings.instance.dialect != "sqlite":
108
+ update_schema_in_hub()
109
+ call_with_fallback_auth(
110
+ update_instance,
111
+ instance_id=settings.instance._id.hex,
112
+ instance_fields={"lamindb_version": lamindb.__version__},
113
+ )
114
+
115
+ @classmethod
116
+ def check(cls) -> bool:
117
+ """Check whether Registry definitions are in sync with migrations."""
118
+ from django.core.management import call_command
119
+
120
+ setup_django(settings.instance)
121
+ try:
122
+ call_command("makemigrations", check_changes=True)
123
+ except SystemExit:
124
+ logger.error(
125
+ "migrations are not in sync with ORMs, please create a migration: lamin"
126
+ " migrate create"
127
+ )
128
+ return False
129
+ return True
130
+
131
+ @classmethod
132
+ def squash(
133
+ cls, package_name, migration_nr, start_migration_nr: str | None = None
134
+ ) -> None:
135
+ """Squash migrations."""
136
+ from django.core.management import call_command
137
+
138
+ setup_django(settings.instance)
139
+ if start_migration_nr is not None:
140
+ call_command(
141
+ "squashmigrations", package_name, start_migration_nr, migration_nr
142
+ )
143
+ else:
144
+ call_command("squashmigrations", package_name, migration_nr)
145
+
146
+ @classmethod
147
+ def show(cls) -> None:
148
+ """Show migrations."""
149
+ from django.core.management import call_command
150
+
151
+ setup_django(settings.instance)
152
+ call_command("showmigrations")
153
+
154
+ @classmethod
155
+ def defined_migrations(cls, latest: bool = False):
156
+ from io import StringIO
157
+
158
+ from django.core.management import call_command
159
+
160
+ def parse_migration_output(output):
161
+ """Parse the output of the showmigrations command to get migration names."""
162
+ lines = output.splitlines()
163
+
164
+ # Initialize an empty dict to store migration names of each module
165
+ migration_names = {}
166
+
167
+ # Process each line
168
+ for line in lines:
169
+ if " " not in line:
170
+ # CLI displays the module name in bold
171
+ name = line.strip().replace("\x1b[1m", "")
172
+ migration_names[name] = []
173
+ continue
174
+ # Strip whitespace and split the line into status and migration name
175
+ migration_name = line.strip().split("] ")[-1].split(" ")[0]
176
+ # The second part is the migration name
177
+ migration_names[name].append(migration_name)
178
+
179
+ return migration_names
180
+
181
+ out = StringIO()
182
+ call_command("showmigrations", stdout=out)
183
+ out.seek(0)
184
+ output = out.getvalue()
185
+ if latest:
186
+ return {k: v[-1] for k, v in parse_migration_output(output).items()}
187
+ else:
188
+ return parse_migration_output(output)
189
+
190
+ @classmethod
191
+ def deployed_migrations(cls, latest: bool = False):
192
+ """Get the list of deployed migrations from Migration table in DB."""
193
+ if latest:
194
+ latest_migrations = {}
195
+ with connection.cursor() as cursor:
196
+ # query to get the latest migration for each app that is not squashed
197
+ cursor.execute(
198
+ """
199
+ SELECT app, name
200
+ FROM django_migrations
201
+ WHERE id IN (
202
+ SELECT MAX(id)
203
+ FROM django_migrations
204
+ WHERE name NOT LIKE '%%_squashed_%%'
205
+ GROUP BY app
206
+ )
207
+ """
208
+ )
209
+ # fetch all the results
210
+ for app, name in cursor.fetchall():
211
+ latest_migrations[app] = name
212
+
213
+ return latest_migrations
214
+ else:
215
+ # Load all migrations using Django's migration loader
216
+ loader = MigrationLoader(connection)
217
+ squashed_replacements = set()
218
+ for _key, migration in loader.disk_migrations.items():
219
+ if hasattr(migration, "replaces"):
220
+ squashed_replacements.update(migration.replaces)
221
+
222
+ deployed_migrations: dict = {}
223
+ with connection.cursor() as cursor:
224
+ cursor.execute(
225
+ """
226
+ SELECT app, name, deployed
227
+ FROM django_migrations
228
+ ORDER BY app, deployed DESC
229
+ """
230
+ )
231
+ for app, name, _deployed in cursor.fetchall():
232
+ # skip migrations that are part of a squashed migration
233
+ if (app, name) in squashed_replacements:
234
+ continue
235
+
236
+ if app not in deployed_migrations:
237
+ deployed_migrations[app] = []
238
+ deployed_migrations[app].append(name)
239
+ return deployed_migrations
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from lamin_utils import logger
4
+
5
+ from .core._settings import settings
6
+ from .core._settings_storage import base62
7
+ from .core.django import setup_django
8
+
9
+
10
+ def register(_test: bool = False):
11
+ """Register an instance on the hub."""
12
+ from ._check_setup import _check_instance_setup
13
+ from .core._hub_core import init_instance as init_instance_hub
14
+ from .core._hub_core import init_storage as init_storage_hub
15
+
16
+ logger.warning("""lamin register will be removed soon""")
17
+
18
+ isettings = settings.instance
19
+ if not _check_instance_setup() and not _test:
20
+ setup_django(isettings)
21
+
22
+ ssettings = settings.instance.storage
23
+ if ssettings._uid is None and _test:
24
+ # because django isn't up, we can't get it from the database
25
+ ssettings._uid = base62(8)
26
+ # cannot yet populate the instance id here
27
+ ssettings._instance_id = None
28
+ # flag auto_populate_instance can be removed once FK migration is over
29
+ init_storage_hub(ssettings, auto_populate_instance=False)
30
+ init_instance_hub(isettings)
31
+ isettings._is_on_hub = True
32
+ isettings._persist()
33
+ if isettings.dialect != "sqlite" and not _test:
34
+ from ._schema_metadata import update_schema_in_hub
35
+
36
+ update_schema_in_hub()