lamindb_setup 1.18.2__py3-none-any.whl → 1.19.1__py3-none-any.whl

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