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
lamindb_setup/_migrate.py CHANGED
@@ -1,331 +1,331 @@
1
- from __future__ import annotations
2
-
3
- import os
4
-
5
- from django.db import connection
6
- from lamin_utils import logger
7
- from packaging import version
8
-
9
- from ._check_setup import _check_instance_setup, disable_auto_connect
10
- from .core._settings import settings
11
- from .core.django import setup_django
12
-
13
-
14
- # for the django-based synching code, see laminhub_rest
15
- def check_whether_migrations_in_sync(db_version_str: str):
16
- from importlib import metadata
17
-
18
- try:
19
- installed_version_str = metadata.version("lamindb")
20
- except metadata.PackageNotFoundError:
21
- return None
22
- if db_version_str is None:
23
- logger.warning("no lamindb version stored to compare with installed version")
24
- return None
25
- installed_version = version.parse(installed_version_str)
26
- db_version = version.parse(db_version_str)
27
- if installed_version.major < db_version.major:
28
- logger.warning(
29
- f"the database ({db_version_str}) is far ahead of your installed lamindb package ({installed_version_str})"
30
- )
31
- logger.important(
32
- f"please update lamindb: pip install lamindb>={db_version.major}"
33
- )
34
- elif (
35
- installed_version.major == db_version.major
36
- and installed_version.minor < db_version.minor
37
- ):
38
- db_version_lower = f"{db_version.major}.{db_version.minor}"
39
- logger.important(
40
- f"the database ({db_version_str}) is ahead of your installed lamindb"
41
- f" package ({installed_version_str})"
42
- )
43
- logger.important(
44
- f"consider updating lamindb: pip install lamindb>={db_version_lower}"
45
- )
46
- elif installed_version.major > db_version.major:
47
- logger.warning(
48
- f"the database ({db_version_str}) is far behind your installed lamindb package"
49
- f" ({installed_version_str})"
50
- )
51
- logger.important(
52
- "if you are an admin, migrate your database: lamin migrate deploy"
53
- )
54
- elif (
55
- installed_version.major == db_version.major
56
- and installed_version.minor > db_version.minor
57
- ):
58
- pass
59
- # if the database is behind by a minor version, we don't want to spam the user
60
- # logger.important(
61
- # f"the database ({db_version_str}) is behind your installed lamindb package"
62
- # f" ({installed_version_str})"
63
- # )
64
- # logger.important("consider migrating your database: lamin migrate deploy")
65
-
66
-
67
- class migrate:
68
- """Manage database migrations.
69
-
70
- Unless you maintain your own schema modules with your own Django models, you won't need this.
71
-
72
- Examples:
73
-
74
- Create a migration::
75
-
76
- import lamindb as ln
77
-
78
- ln.setup.migrate.create()
79
-
80
- Deploy a migration::
81
-
82
- ln.setup.migrate.deploy()
83
-
84
- Check migration consistency::
85
-
86
- ln.setup.migrate.check()
87
-
88
- See Also:
89
- Migrate an instance via the CLI, see `here <https://docs.lamin.ai/cli#migrate>`__.
90
-
91
- """
92
-
93
- @classmethod
94
- @disable_auto_connect
95
- def create(cls) -> None:
96
- """Create a migration."""
97
- setup_django(settings.instance, create_migrations=True)
98
-
99
- @classmethod
100
- def deploy(cls, package_name: str | None = None, number: int | None = None) -> None:
101
- assert settings._instance_exists, (
102
- "Not connected to an instance, please connect to migrate."
103
- )
104
-
105
- # NOTE: this is a temporary solution to avoid breaking tests
106
- LAMIN_MIGRATE_ON_LAMBDA = (
107
- os.getenv("LAMIN_MIGRATE_ON_LAMBDA", "false") == "true"
108
- )
109
- isettings = settings.instance
110
-
111
- if isettings.is_on_hub and LAMIN_MIGRATE_ON_LAMBDA:
112
- # dynamic import to avoid importing the heavy httpx at root
113
- import httpx
114
-
115
- response = httpx.post(
116
- f"{isettings.api_url}/instances/{isettings._id}/migrate",
117
- headers={"Authorization": f"Bearer {settings.user.access_token}"},
118
- timeout=None, # this can take time
119
- )
120
- if response.status_code != 200:
121
- raise Exception(f"Failed to migrate instance: {response.text}")
122
- else:
123
- cls._deploy(package_name=package_name, number=number)
124
-
125
- @classmethod
126
- def _deploy(
127
- cls, package_name: str | None = None, number: int | None = None
128
- ) -> None:
129
- """Deploy a migration."""
130
- from lamindb_setup._connect_instance import connect
131
- from lamindb_setup._schema_metadata import update_schema_in_hub
132
- from lamindb_setup.core._hub_client import call_with_fallback_auth
133
- from lamindb_setup.core._hub_crud import (
134
- update_instance,
135
- )
136
-
137
- isettings = settings.instance
138
- is_managed_by_hub = isettings.is_managed_by_hub
139
- is_on_hub = is_managed_by_hub or isettings.is_on_hub
140
-
141
- if is_managed_by_hub and "root" not in isettings.db:
142
- # ensure we connect with the root user
143
- connect(use_root_db_user=True)
144
- assert "root" in (instance_db := settings.instance.db), instance_db
145
- if is_on_hub:
146
- # we need lamindb to be installed, otherwise we can't populate the version
147
- # information in the hub
148
- # this also connects
149
- import lamindb
150
- # this is needed to avoid connecting on importing apps inside setup_django process
151
- setup_django_disable_autoconnect = disable_auto_connect(setup_django)
152
- # this sets up django and deploys the migrations
153
- if package_name is not None and number is not None:
154
- setup_django_disable_autoconnect(
155
- isettings,
156
- deploy_migrations=True,
157
- appname_number=(package_name, number),
158
- )
159
- else:
160
- setup_django_disable_autoconnect(isettings, deploy_migrations=True)
161
- # this populates the hub
162
- if is_on_hub:
163
- logger.important(f"updating lamindb version in hub: {lamindb.__version__}")
164
- if isettings.dialect != "sqlite":
165
- update_schema_in_hub()
166
- call_with_fallback_auth(
167
- update_instance,
168
- instance_id=isettings._id.hex,
169
- instance_fields={"lamindb_version": lamindb.__version__},
170
- )
171
-
172
- @classmethod
173
- @disable_auto_connect
174
- def check(cls) -> bool:
175
- """Check whether Registry definitions are in sync with migrations."""
176
- import io
177
-
178
- from django.core.management import call_command
179
-
180
- setup_django(settings.instance)
181
-
182
- # Capture stdout/stderr to show what migrations are needed if check fails
183
- stdout = io.StringIO()
184
- stderr = io.StringIO()
185
-
186
- try:
187
- call_command(
188
- "makemigrations", check_changes=True, stdout=stdout, stderr=stderr
189
- )
190
- except SystemExit:
191
- logger.error(
192
- "migrations are not in sync with ORMs, please create a migration: lamin"
193
- " migrate create"
194
- )
195
- # Print captured output from the check
196
- if stdout.getvalue():
197
- logger.error(f"makemigrations --check stdout:\n{stdout.getvalue()}")
198
- if stderr.getvalue():
199
- logger.error(f"makemigrations --check stderr:\n{stderr.getvalue()}")
200
-
201
- # Run makemigrations --dry-run to show what would be created
202
- stdout2 = io.StringIO()
203
- stderr2 = io.StringIO()
204
- try:
205
- call_command(
206
- "makemigrations", dry_run=True, stdout=stdout2, stderr=stderr2
207
- )
208
- except SystemExit:
209
- pass
210
- if stdout2.getvalue():
211
- logger.error(f"makemigrations --dry-run stdout:\n{stdout2.getvalue()}")
212
- if stderr2.getvalue():
213
- logger.error(f"makemigrations --dry-run stderr:\n{stderr2.getvalue()}")
214
-
215
- return False
216
- return True
217
-
218
- @classmethod
219
- @disable_auto_connect
220
- def squash(
221
- cls, package_name, migration_nr, start_migration_nr: str | None = None
222
- ) -> None:
223
- """Squash migrations."""
224
- from django.core.management import call_command
225
-
226
- setup_django(settings.instance)
227
- if start_migration_nr is not None:
228
- call_command(
229
- "squashmigrations", package_name, start_migration_nr, migration_nr
230
- )
231
- else:
232
- call_command("squashmigrations", package_name, migration_nr)
233
-
234
- @classmethod
235
- @disable_auto_connect
236
- def show(cls) -> None:
237
- """Show migrations."""
238
- from django.core.management import call_command
239
-
240
- setup_django(settings.instance)
241
- call_command("showmigrations")
242
-
243
- @classmethod
244
- def defined_migrations(cls, latest: bool = False):
245
- from io import StringIO
246
-
247
- from django.core.management import call_command
248
-
249
- def parse_migration_output(output):
250
- """Parse the output of the showmigrations command to get migration names."""
251
- lines = output.splitlines()
252
-
253
- # Initialize an empty dict to store migration names of each module
254
- migration_names = {}
255
-
256
- # Process each line
257
- for line in lines:
258
- if " " not in line:
259
- # CLI displays the module name in bold
260
- name = line.strip().replace("\x1b[1m", "")
261
- migration_names[name] = []
262
- continue
263
- # Strip whitespace and split the line into status and migration name
264
- migration_name = line.strip().split("] ")[-1].split(" ")[0]
265
- # The second part is the migration name
266
- migration_names[name].append(migration_name)
267
-
268
- return migration_names
269
-
270
- out = StringIO()
271
- call_command("showmigrations", stdout=out)
272
- out.seek(0)
273
- output = out.getvalue()
274
- if latest:
275
- return {k: v[-1] for k, v in parse_migration_output(output).items()}
276
- else:
277
- return parse_migration_output(output)
278
-
279
- @classmethod
280
- def deployed_migrations(cls, latest: bool = False):
281
- """Get the list of deployed migrations from Migration table in DB."""
282
- if latest:
283
- latest_migrations = {}
284
- with connection.cursor() as cursor:
285
- # query to get the latest migration for each app that is not squashed
286
- cursor.execute(
287
- """
288
- SELECT app, name
289
- FROM django_migrations
290
- WHERE id IN (
291
- SELECT MAX(id)
292
- FROM django_migrations
293
- WHERE name NOT LIKE '%%_squashed_%%'
294
- GROUP BY app
295
- )
296
- """
297
- )
298
- # fetch all the results
299
- for app, name in cursor.fetchall():
300
- latest_migrations[app] = name
301
-
302
- return latest_migrations
303
- else:
304
- # import dynamically to avoid importing the heavy django.db.migrations at the root
305
- from django.db.migrations.loader import MigrationLoader
306
-
307
- # Load all migrations using Django's migration loader
308
- loader = MigrationLoader(connection)
309
- squashed_replacements = set()
310
- for _key, migration in loader.disk_migrations.items():
311
- if hasattr(migration, "replaces"):
312
- squashed_replacements.update(migration.replaces)
313
-
314
- deployed_migrations: dict = {}
315
- with connection.cursor() as cursor:
316
- cursor.execute(
317
- """
318
- SELECT app, name, deployed
319
- FROM django_migrations
320
- ORDER BY app, deployed DESC
321
- """
322
- )
323
- for app, name, _deployed in cursor.fetchall():
324
- # skip migrations that are part of a squashed migration
325
- if (app, name) in squashed_replacements:
326
- continue
327
-
328
- if app not in deployed_migrations:
329
- deployed_migrations[app] = []
330
- deployed_migrations[app].append(name)
331
- return deployed_migrations
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ from django.db import connection
6
+ from lamin_utils import logger
7
+ from packaging import version
8
+
9
+ from ._check_setup import _check_instance_setup, disable_auto_connect
10
+ from .core._settings import settings
11
+ from .core.django import setup_django
12
+
13
+
14
+ # for the django-based synching code, see laminhub_rest
15
+ def check_whether_migrations_in_sync(db_version_str: str):
16
+ from importlib import metadata
17
+
18
+ try:
19
+ installed_version_str = metadata.version("lamindb")
20
+ except metadata.PackageNotFoundError:
21
+ return None
22
+ if db_version_str is None:
23
+ logger.warning("no lamindb version stored to compare with installed version")
24
+ return None
25
+ installed_version = version.parse(installed_version_str)
26
+ db_version = version.parse(db_version_str)
27
+ if installed_version.major < db_version.major:
28
+ logger.warning(
29
+ f"the database ({db_version_str}) is far ahead of your installed lamindb package ({installed_version_str})"
30
+ )
31
+ logger.important(
32
+ f"please update lamindb: pip install lamindb>={db_version.major}"
33
+ )
34
+ elif (
35
+ installed_version.major == db_version.major
36
+ and installed_version.minor < db_version.minor
37
+ ):
38
+ db_version_lower = f"{db_version.major}.{db_version.minor}"
39
+ logger.important(
40
+ f"the database ({db_version_str}) is ahead of your installed lamindb"
41
+ f" package ({installed_version_str})"
42
+ )
43
+ logger.important(
44
+ f"consider updating lamindb: pip install lamindb>={db_version_lower}"
45
+ )
46
+ elif installed_version.major > db_version.major:
47
+ logger.warning(
48
+ f"the database ({db_version_str}) is far behind your installed lamindb package"
49
+ f" ({installed_version_str})"
50
+ )
51
+ logger.important(
52
+ "if you are an admin, migrate your database: lamin migrate deploy"
53
+ )
54
+ elif (
55
+ installed_version.major == db_version.major
56
+ and installed_version.minor > db_version.minor
57
+ ):
58
+ pass
59
+ # if the database is behind by a minor version, we don't want to spam the user
60
+ # logger.important(
61
+ # f"the database ({db_version_str}) is behind your installed lamindb package"
62
+ # f" ({installed_version_str})"
63
+ # )
64
+ # logger.important("consider migrating your database: lamin migrate deploy")
65
+
66
+
67
+ class migrate:
68
+ """Manage database migrations.
69
+
70
+ Unless you maintain your own schema modules with your own Django models, you won't need this.
71
+
72
+ Examples:
73
+
74
+ Create a migration::
75
+
76
+ import lamindb as ln
77
+
78
+ ln.setup.migrate.create()
79
+
80
+ Deploy a migration::
81
+
82
+ ln.setup.migrate.deploy()
83
+
84
+ Check migration consistency::
85
+
86
+ ln.setup.migrate.check()
87
+
88
+ See Also:
89
+ Migrate an instance via the CLI, see `here <https://docs.lamin.ai/cli#migrate>`__.
90
+
91
+ """
92
+
93
+ @classmethod
94
+ @disable_auto_connect
95
+ def create(cls) -> None:
96
+ """Create a migration."""
97
+ setup_django(settings.instance, create_migrations=True)
98
+
99
+ @classmethod
100
+ def deploy(cls, package_name: str | None = None, number: int | None = None) -> None:
101
+ assert settings._instance_exists, (
102
+ "Not connected to an instance, please connect to migrate."
103
+ )
104
+
105
+ # NOTE: this is a temporary solution to avoid breaking tests
106
+ LAMIN_MIGRATE_ON_LAMBDA = (
107
+ os.getenv("LAMIN_MIGRATE_ON_LAMBDA", "false") == "true"
108
+ )
109
+ isettings = settings.instance
110
+
111
+ if isettings.is_on_hub and LAMIN_MIGRATE_ON_LAMBDA:
112
+ # dynamic import to avoid importing the heavy httpx at root
113
+ import httpx
114
+
115
+ response = httpx.post(
116
+ f"{isettings.api_url}/instances/{isettings._id}/migrate",
117
+ headers={"Authorization": f"Bearer {settings.user.access_token}"},
118
+ timeout=None, # this can take time
119
+ )
120
+ if response.status_code != 200:
121
+ raise Exception(f"Failed to migrate instance: {response.text}")
122
+ else:
123
+ cls._deploy(package_name=package_name, number=number)
124
+
125
+ @classmethod
126
+ def _deploy(
127
+ cls, package_name: str | None = None, number: int | None = None
128
+ ) -> None:
129
+ """Deploy a migration."""
130
+ from lamindb_setup._connect_instance import connect
131
+ from lamindb_setup._schema_metadata import update_schema_in_hub
132
+ from lamindb_setup.core._hub_client import call_with_fallback_auth
133
+ from lamindb_setup.core._hub_crud import (
134
+ update_instance,
135
+ )
136
+
137
+ isettings = settings.instance
138
+ is_managed_by_hub = isettings.is_managed_by_hub
139
+ is_on_hub = is_managed_by_hub or isettings.is_on_hub
140
+
141
+ if is_managed_by_hub and "root" not in isettings.db:
142
+ # ensure we connect with the root user
143
+ connect(use_root_db_user=True)
144
+ assert "root" in (instance_db := settings.instance.db), instance_db
145
+ if is_on_hub:
146
+ # we need lamindb to be installed, otherwise we can't populate the version
147
+ # information in the hub
148
+ # this also connects
149
+ import lamindb
150
+ # this is needed to avoid connecting on importing apps inside setup_django process
151
+ setup_django_disable_autoconnect = disable_auto_connect(setup_django)
152
+ # this sets up django and deploys the migrations
153
+ if package_name is not None and number is not None:
154
+ setup_django_disable_autoconnect(
155
+ isettings,
156
+ deploy_migrations=True,
157
+ appname_number=(package_name, number),
158
+ )
159
+ else:
160
+ setup_django_disable_autoconnect(isettings, deploy_migrations=True)
161
+ # this populates the hub
162
+ if is_on_hub:
163
+ logger.important(f"updating lamindb version in hub: {lamindb.__version__}")
164
+ if isettings.dialect != "sqlite":
165
+ update_schema_in_hub()
166
+ call_with_fallback_auth(
167
+ update_instance,
168
+ instance_id=isettings._id.hex,
169
+ instance_fields={"lamindb_version": lamindb.__version__},
170
+ )
171
+
172
+ @classmethod
173
+ @disable_auto_connect
174
+ def check(cls) -> bool:
175
+ """Check whether Registry definitions are in sync with migrations."""
176
+ import io
177
+
178
+ from django.core.management import call_command
179
+
180
+ setup_django(settings.instance)
181
+
182
+ # Capture stdout/stderr to show what migrations are needed if check fails
183
+ stdout = io.StringIO()
184
+ stderr = io.StringIO()
185
+
186
+ try:
187
+ call_command(
188
+ "makemigrations", check_changes=True, stdout=stdout, stderr=stderr
189
+ )
190
+ except SystemExit:
191
+ logger.error(
192
+ "migrations are not in sync with ORMs, please create a migration: lamin"
193
+ " migrate create"
194
+ )
195
+ # Print captured output from the check
196
+ if stdout.getvalue():
197
+ logger.error(f"makemigrations --check stdout:\n{stdout.getvalue()}")
198
+ if stderr.getvalue():
199
+ logger.error(f"makemigrations --check stderr:\n{stderr.getvalue()}")
200
+
201
+ # Run makemigrations --dry-run to show what would be created
202
+ stdout2 = io.StringIO()
203
+ stderr2 = io.StringIO()
204
+ try:
205
+ call_command(
206
+ "makemigrations", dry_run=True, stdout=stdout2, stderr=stderr2
207
+ )
208
+ except SystemExit:
209
+ pass
210
+ if stdout2.getvalue():
211
+ logger.error(f"makemigrations --dry-run stdout:\n{stdout2.getvalue()}")
212
+ if stderr2.getvalue():
213
+ logger.error(f"makemigrations --dry-run stderr:\n{stderr2.getvalue()}")
214
+
215
+ return False
216
+ return True
217
+
218
+ @classmethod
219
+ @disable_auto_connect
220
+ def squash(
221
+ cls, package_name, migration_nr, start_migration_nr: str | None = None
222
+ ) -> None:
223
+ """Squash migrations."""
224
+ from django.core.management import call_command
225
+
226
+ setup_django(settings.instance)
227
+ if start_migration_nr is not None:
228
+ call_command(
229
+ "squashmigrations", package_name, start_migration_nr, migration_nr
230
+ )
231
+ else:
232
+ call_command("squashmigrations", package_name, migration_nr)
233
+
234
+ @classmethod
235
+ @disable_auto_connect
236
+ def show(cls) -> None:
237
+ """Show migrations."""
238
+ from django.core.management import call_command
239
+
240
+ setup_django(settings.instance)
241
+ call_command("showmigrations")
242
+
243
+ @classmethod
244
+ def defined_migrations(cls, latest: bool = False):
245
+ from io import StringIO
246
+
247
+ from django.core.management import call_command
248
+
249
+ def parse_migration_output(output):
250
+ """Parse the output of the showmigrations command to get migration names."""
251
+ lines = output.splitlines()
252
+
253
+ # Initialize an empty dict to store migration names of each module
254
+ migration_names = {}
255
+
256
+ # Process each line
257
+ for line in lines:
258
+ if " " not in line:
259
+ # CLI displays the module name in bold
260
+ name = line.strip().replace("\x1b[1m", "")
261
+ migration_names[name] = []
262
+ continue
263
+ # Strip whitespace and split the line into status and migration name
264
+ migration_name = line.strip().split("] ")[-1].split(" ")[0]
265
+ # The second part is the migration name
266
+ migration_names[name].append(migration_name)
267
+
268
+ return migration_names
269
+
270
+ out = StringIO()
271
+ call_command("showmigrations", stdout=out)
272
+ out.seek(0)
273
+ output = out.getvalue()
274
+ if latest:
275
+ return {k: v[-1] for k, v in parse_migration_output(output).items()}
276
+ else:
277
+ return parse_migration_output(output)
278
+
279
+ @classmethod
280
+ def deployed_migrations(cls, latest: bool = False):
281
+ """Get the list of deployed migrations from Migration table in DB."""
282
+ if latest:
283
+ latest_migrations = {}
284
+ with connection.cursor() as cursor:
285
+ # query to get the latest migration for each app that is not squashed
286
+ cursor.execute(
287
+ """
288
+ SELECT app, name
289
+ FROM django_migrations
290
+ WHERE id IN (
291
+ SELECT MAX(id)
292
+ FROM django_migrations
293
+ WHERE name NOT LIKE '%%_squashed_%%'
294
+ GROUP BY app
295
+ )
296
+ """
297
+ )
298
+ # fetch all the results
299
+ for app, name in cursor.fetchall():
300
+ latest_migrations[app] = name
301
+
302
+ return latest_migrations
303
+ else:
304
+ # import dynamically to avoid importing the heavy django.db.migrations at the root
305
+ from django.db.migrations.loader import MigrationLoader
306
+
307
+ # Load all migrations using Django's migration loader
308
+ loader = MigrationLoader(connection)
309
+ squashed_replacements = set()
310
+ for _key, migration in loader.disk_migrations.items():
311
+ if hasattr(migration, "replaces"):
312
+ squashed_replacements.update(migration.replaces)
313
+
314
+ deployed_migrations: dict = {}
315
+ with connection.cursor() as cursor:
316
+ cursor.execute(
317
+ """
318
+ SELECT app, name, deployed
319
+ FROM django_migrations
320
+ ORDER BY app, deployed DESC
321
+ """
322
+ )
323
+ for app, name, _deployed in cursor.fetchall():
324
+ # skip migrations that are part of a squashed migration
325
+ if (app, name) in squashed_replacements:
326
+ continue
327
+
328
+ if app not in deployed_migrations:
329
+ deployed_migrations[app] = []
330
+ deployed_migrations[app].append(name)
331
+ return deployed_migrations