django-migration-zero 2.1.0__tar.gz → 2.2.0__tar.gz

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 (73) hide show
  1. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.ambient-package-update/metadata.py +6 -3
  2. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.github/workflows/ci.yml +7 -8
  3. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.pre-commit-config.yaml +4 -4
  4. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/CHANGES.md +9 -0
  5. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/PKG-INFO +7 -7
  6. django_migration_zero-2.2.0/SECURITY.md +18 -0
  7. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/__init__.py +1 -1
  8. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/helpers/file_system.py +2 -3
  9. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/managers.py +7 -16
  10. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/services/deployment.py +4 -3
  11. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/conf.py +0 -2
  12. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/pyproject.toml +24 -18
  13. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/services/test_deployment.py +56 -1
  14. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/test_managers.py +2 -1
  15. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.ambient-package-update/templates/snippets/content.tpl +0 -0
  16. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.ambient-package-update/templates/snippets/installation.tpl +0 -0
  17. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.ambient-package-update/templates/snippets/tagline.tpl +0 -0
  18. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.coveragerc +0 -0
  19. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.editorconfig +0 -0
  20. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.gitignore +0 -0
  21. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/.readthedocs.yaml +0 -0
  22. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/LICENSE.md +0 -0
  23. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/MANIFEST.in +0 -0
  24. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/README.md +0 -0
  25. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/admin.py +0 -0
  26. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/apps.py +0 -0
  27. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/exceptions.py +0 -0
  28. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/helpers/__init__.py +0 -0
  29. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/helpers/logger.py +0 -0
  30. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/locale/de/LC_MESSAGES/django.mo +0 -0
  31. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/locale/de/LC_MESSAGES/django.po +0 -0
  32. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/management/__init__.py +0 -0
  33. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/management/commands/__init__.py +0 -0
  34. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/management/commands/handle_migration_zero_reset.py +0 -0
  35. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/management/commands/reset_local_migration_files.py +0 -0
  36. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/migrations/0001_initial.py +0 -0
  37. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/migrations/__init__.py +0 -0
  38. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/models.py +0 -0
  39. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/services/__init__.py +0 -0
  40. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/django_migration_zero/services/local.py +0 -0
  41. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/Makefile +0 -0
  42. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/features/01_introduction.md +0 -0
  43. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/features/02_configuration.md +0 -0
  44. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/features/03_workflow.md +0 -0
  45. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/features/04_common_issues_and_solutions.md +0 -0
  46. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/features/05_architecture.md +0 -0
  47. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/features/06_management_commands.md +0 -0
  48. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/features/07_changelog.rst +0 -0
  49. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/index.rst +0 -0
  50. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/docs/make.bat +0 -0
  51. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/manage.py +0 -0
  52. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/scripts/unix/install_requirements.sh +0 -0
  53. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/scripts/unix/publish_to_pypi.sh +0 -0
  54. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/scripts/windows/install_requirements.ps1 +0 -0
  55. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/scripts/windows/publish_to_pypi.ps1 +0 -0
  56. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/settings.py +0 -0
  57. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/setup.cfg +0 -0
  58. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/testapp/__init__.py +0 -0
  59. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/testapp/nested_app/__init__.py +0 -0
  60. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/testapp/nested_app/migrations/__init__.py +0 -0
  61. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/testapp/urls.py +0 -0
  62. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/__init__.py +0 -0
  63. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/helpers/__init__.py +0 -0
  64. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/helpers/test_file_system.py +0 -0
  65. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/helpers/test_logger.py +0 -0
  66. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/management/__init__.py +0 -0
  67. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/management/commands/__init__.py +0 -0
  68. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/management/commands/test_handle_migration_zero_reset.py +0 -0
  69. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/management/commands/test_reset_local_migration_files.py +0 -0
  70. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/services/__init__.py +0 -0
  71. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/services/test_local.py +0 -0
  72. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/test_admin.py +0 -0
  73. {django_migration_zero-2.1.0 → django_migration_zero-2.2.0}/tests/test_models.py +0 -0
@@ -8,10 +8,11 @@ from ambient_package_update.metadata.constants import (
8
8
  from ambient_package_update.metadata.maintainer import PackageMaintainer
9
9
  from ambient_package_update.metadata.package import PackageMetadata
10
10
  from ambient_package_update.metadata.readme import ReadmeContent
11
+ from ambient_package_update.metadata.ruff_ignored_inspection import RuffIgnoredInspection
11
12
 
12
13
  METADATA = PackageMetadata(
13
14
  package_name="django_migration_zero",
14
- module_name="django_migration_zero",
15
+ github_package_group="ambient-innovation",
15
16
  authors=[
16
17
  PackageAuthor(
17
18
  name="Ambient Digital",
@@ -26,12 +27,14 @@ METADATA = PackageMetadata(
26
27
  has_migrations=True,
27
28
  readme_content=ReadmeContent(uses_internationalisation=True),
28
29
  dependencies=[
29
- "Django>=3.2",
30
+ f"Django>={SUPPORTED_DJANGO_VERSIONS[0]}",
30
31
  ],
31
32
  supported_django_versions=SUPPORTED_DJANGO_VERSIONS,
32
33
  supported_python_versions=SUPPORTED_PYTHON_VERSIONS,
33
34
  optional_dependencies={
34
35
  "dev": [*DEV_DEPENDENCIES, "unittest-parametrize~=1.3"],
35
36
  },
36
- ruff_ignore_list=[],
37
+ ruff_ignore_list=[
38
+ RuffIgnoredInspection(key="TRY003", comment="Avoid specifying long messages outside the exception class"),
39
+ ],
37
40
  )
@@ -42,12 +42,10 @@ jobs:
42
42
  runs-on: ubuntu-22.04
43
43
  strategy:
44
44
  matrix:
45
- python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', ]
45
+ python-version: ['3.9', '3.10', '3.11', '3.12', ]
46
46
  django-version: ['42', '50', ]
47
47
 
48
48
  exclude:
49
- - python-version: '3.8'
50
- django-version: 50
51
49
  - python-version: '3.9'
52
50
  django-version: 50
53
51
 
@@ -64,9 +62,9 @@ jobs:
64
62
  TOXENV: django${{ matrix.django-version }}
65
63
  run: tox
66
64
  - name: Upload coverage data
67
- uses: actions/upload-artifact@v3
65
+ uses: actions/upload-artifact@v4
68
66
  with:
69
- name: coverage-data
67
+ name: coverage-data-${{ matrix.python-version }}-${{ matrix.django-version }}
70
68
  path: '.coverage*'
71
69
 
72
70
  coverage:
@@ -84,11 +82,12 @@ jobs:
84
82
  run: python -m pip install --upgrade coverage[toml]
85
83
 
86
84
  - name: Download data
87
- uses: actions/download-artifact@v3
85
+ uses: actions/download-artifact@v4
88
86
  with:
89
- name: coverage-data
87
+ pattern: coverage-data*
88
+ merge-multiple: true
90
89
 
91
- - name: Combine coverage and fail if it's <100%
90
+ - name: Combine coverage and fail if it's <100.0%
92
91
  run: |
93
92
  python -m coverage html --skip-covered --skip-empty
94
93
  python -m coverage report --fail-under=100.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  repos:
5
5
  - repo: https://github.com/astral-sh/ruff-pre-commit
6
- rev: v0.4.8
6
+ rev: v0.5.2
7
7
  hooks:
8
8
  # Run the Ruff linter.
9
9
  - id: ruff
@@ -12,7 +12,7 @@ repos:
12
12
  - id: ruff-format
13
13
 
14
14
  - repo: https://github.com/adamchainz/blacken-docs
15
- rev: 1.16.0
15
+ rev: 1.18.0
16
16
  hooks:
17
17
  - id: blacken-docs
18
18
  additional_dependencies:
@@ -23,11 +23,11 @@ repos:
23
23
  rev: v3.16.0
24
24
  hooks:
25
25
  - id: pyupgrade
26
- args: [ --py38-plus ]
26
+ args: [ --py39-plus ]
27
27
  stages: [ push ]
28
28
 
29
29
  - repo: https://github.com/adamchainz/django-upgrade
30
- rev: 1.18.0
30
+ rev: 1.19.0
31
31
  hooks:
32
32
  - id: django-upgrade
33
33
  args: [--target-version, "4.2"]
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ * *2.2.0* (2024-07-18)
4
+ * Use ORM to reset `django_migrations` table
5
+ * Lock rows to enable parallel deployments
6
+ * Dropped Python 3.8 support
7
+ * Added multiple ruff linters and updated packages
8
+ * Updated GitHub actions
9
+ * Added SECURITY.md
10
+ * Internal updates via `ambient-package-update`
11
+
3
12
  * *2.1.0* (2024-07-09)
4
13
  * Discover apps in nested directories
5
14
  * Use `BASE_DIR` instead of `MIGRATION_ZERO_APPS_DIR`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
- Name: django-migration-zero
3
- Version: 2.1.0
2
+ Name: django_migration_zero
3
+ Version: 2.2.0
4
4
  Summary: Holistic implementation of "migration zero" pattern for Django covering local changes and in-production database adjustments.
5
5
  Author-email: Ambient Digital <hello@ambient.digital>
6
6
  Description-Content-Type: text/markdown
@@ -15,26 +15,26 @@ Classifier: Natural Language :: English
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.8
19
18
  Classifier: Programming Language :: Python :: 3.9
20
19
  Classifier: Programming Language :: Python :: 3.10
21
20
  Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
22
  Classifier: Topic :: Utilities
24
- Requires-Dist: Django>=3.2
23
+ Requires-Dist: Django>=4.2
25
24
  Requires-Dist: typer~=0.9 ; extra == "dev"
26
25
  Requires-Dist: freezegun~=1.3 ; extra == "dev"
27
26
  Requires-Dist: pytest-django~=4.7 ; extra == "dev"
28
27
  Requires-Dist: pytest-mock~=3.12 ; extra == "dev"
29
28
  Requires-Dist: coverage~=7.3 ; extra == "dev"
30
- Requires-Dist: pre-commit~=3.5 ; extra == "dev"
31
- Requires-Dist: ruff~=0.1.7 ; extra == "dev"
29
+ Requires-Dist: pre-commit~=3.7 ; extra == "dev"
30
+ Requires-Dist: ruff~=0.4 ; extra == "dev"
32
31
  Requires-Dist: sphinx~=7.1 ; extra == "dev"
33
32
  Requires-Dist: sphinx-rtd-theme~=2.0 ; extra == "dev"
34
33
  Requires-Dist: m2r2==0.3.3.post2 ; extra == "dev"
35
34
  Requires-Dist: mistune<2.0.0 ; extra == "dev"
36
35
  Requires-Dist: flit~=3.9 ; extra == "dev"
37
- Requires-Dist: ambient-package-update~=24.6.4 ; extra == "dev"
36
+ Requires-Dist: keyring~=25.2 ; extra == "dev"
37
+ Requires-Dist: ambient-package-update ; extra == "dev"
38
38
  Requires-Dist: unittest-parametrize~=1.3 ; extra == "dev"
39
39
  Project-URL: Bugtracker, https://github.com/ambient-innovation/django-migration-zero/issues
40
40
  Project-URL: Changelog, https://django-migration-zero.readthedocs.io/en/latest/features/changelog.html
@@ -0,0 +1,18 @@
1
+ # Reporting Security Issues
2
+
3
+ The maintainers of this package take security bugs seriously. We appreciate your efforts to responsibly
4
+ disclose your findings, and will make every effort to acknowledge your contributions.
5
+
6
+ To report a security issue, please use the GitHub Security
7
+ Advisory ["Report a Vulnerability"](https://github.com/ambient-innovation/django-migration-zero/security/advisories/new)
8
+ tab.
9
+
10
+ The maintainers will send a response indicating the next steps in handling your report. After the initial reply to
11
+ your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask
12
+ for additional information or guidance.
13
+
14
+ Report security bugs in third-party modules to the person or team maintaining the module.
15
+
16
+ ## Source
17
+
18
+ This file was inspired by: https://github.com/electron/electron/blob/main/SECURITY.md
@@ -3,4 +3,4 @@
3
3
  Holistic implementation of "migration zero" pattern for Django covering local changes and in-production database adjustments.
4
4
  """
5
5
 
6
- __version__ = "2.1.0"
6
+ __version__ = "2.2.0"
@@ -2,7 +2,6 @@ import os
2
2
  import re
3
3
  from os.path import isdir
4
4
  from pathlib import Path
5
- from typing import List
6
5
 
7
6
  from django.apps import apps
8
7
  from django.apps.config import AppConfig
@@ -20,7 +19,7 @@ def build_migration_directory_path(*, app_path: Path) -> Path:
20
19
  return app_path / "migrations"
21
20
 
22
21
 
23
- def get_local_django_apps() -> List[AppConfig]:
22
+ def get_local_django_apps() -> list[AppConfig]:
24
23
  """
25
24
  Iterate all installed Django apps and detect local ones.
26
25
  """
@@ -46,7 +45,7 @@ def has_migration_directory(*, app_path: Path) -> bool:
46
45
  return True if isdir(possible_migration_dir) else False
47
46
 
48
47
 
49
- def get_migration_files(*, app_label: str, app_path: Path, exclude_initials: bool = False) -> List[str]:
48
+ def get_migration_files(*, app_label: str, app_path: Path, exclude_initials: bool = False) -> list[str]:
50
49
  """
51
50
  Returns a list of all migration files detected in the given Django app.
52
51
  """
@@ -1,35 +1,26 @@
1
+ from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
1
2
  from django.db import ProgrammingError, models
2
3
 
3
4
  from django_migration_zero.exceptions import MissingMigrationZeroConfigRecordError
4
5
  from django_migration_zero.helpers.logger import get_logger
5
6
 
6
7
 
7
- class MigrationZeroConfigurationQuerySet(models.QuerySet):
8
- pass
9
-
10
-
11
8
  class MigrationZeroConfigurationManager(models.Manager):
12
9
  def fetch_singleton(self) -> None:
13
10
  logger = get_logger()
14
11
  try:
15
- number_records = self.count()
12
+ config_singleton = self.select_for_update().get()
16
13
  except ProgrammingError:
17
14
  logger.warning(
18
15
  "The migration zero table is missing. This might be ok for the first installation of "
19
16
  '"django-migration-zero" but if you see this warning after that point, something went sideways.'
20
17
  )
21
- return None
22
-
23
- if number_records > 1:
18
+ config_singleton = None
19
+ except MultipleObjectsReturned as e:
24
20
  raise MissingMigrationZeroConfigRecordError(
25
21
  "Too many configuration records detected. There can only be one."
26
- )
27
-
28
- config_singleton = self.all().first()
29
- if not config_singleton:
30
- raise MissingMigrationZeroConfigRecordError("No configuration record found in the database.")
22
+ ) from e
23
+ except ObjectDoesNotExist as e:
24
+ raise MissingMigrationZeroConfigRecordError("No configuration record found in the database.") from e
31
25
 
32
26
  return config_singleton
33
-
34
-
35
- MigrationZeroConfigurationManager = MigrationZeroConfigurationManager.from_queryset(MigrationZeroConfigurationQuerySet)
@@ -1,7 +1,8 @@
1
1
  from logging import Logger
2
2
 
3
3
  from django.core.management import call_command
4
- from django.db import connections
4
+ from django.db import transaction
5
+ from django.db.migrations.recorder import MigrationRecorder
5
6
 
6
7
  from django_migration_zero.exceptions import InvalidMigrationTreeError
7
8
  from django_migration_zero.helpers.logger import get_logger
@@ -20,6 +21,7 @@ class DatabasePreparationService:
20
21
 
21
22
  self.logger = get_logger()
22
23
 
24
+ @transaction.atomic
23
25
  def process(self):
24
26
  self.logger.info("Starting migration zero database adjustments...")
25
27
 
@@ -34,8 +36,7 @@ class DatabasePreparationService:
34
36
  # records of the other ones
35
37
  self.logger.info("Resetting migration history for all apps...")
36
38
 
37
- with connections["default"].cursor() as cursor:
38
- cursor.execute("DELETE FROM `django_migrations`")
39
+ MigrationRecorder.Migration.objects.all().delete()
39
40
 
40
41
  # Apply migrations via fake because the database is already up-to-date
41
42
  self.logger.info("Populating migration history.")
@@ -67,8 +67,6 @@ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
67
67
 
68
68
  # The theme to use for HTML and HTML Help pages. See the documentation for
69
69
  # a list of builtin themes.
70
- #
71
- # html_theme = "alabaster"
72
70
  html_theme = "sphinx_rtd_theme"
73
71
  html_theme_options = {
74
72
  "display_version": False,
@@ -3,7 +3,7 @@ requires = ["flit_core>=3.4"]
3
3
  build-backend = "flit_core.buildapi"
4
4
 
5
5
  [project]
6
- name = "django-migration-zero"
6
+ name = "django_migration_zero"
7
7
  authors = [
8
8
  {'name' = 'Ambient Digital', 'email' = 'hello@ambient.digital'},
9
9
  ]
@@ -20,7 +20,6 @@ classifiers = [
20
20
  "Operating System :: OS Independent",
21
21
  "Programming Language :: Python",
22
22
  "Programming Language :: Python :: 3",
23
- "Programming Language :: Python :: 3.8",
24
23
  "Programming Language :: Python :: 3.9",
25
24
  "Programming Language :: Python :: 3.10",
26
25
  "Programming Language :: Python :: 3.11",
@@ -30,7 +29,7 @@ classifiers = [
30
29
  dynamic = ["version", "description"]
31
30
  license = {"file" = "LICENSE.md"}
32
31
  dependencies = [
33
- 'Django>=3.2',
32
+ 'Django>=4.2',
34
33
  ]
35
34
 
36
35
 
@@ -41,14 +40,15 @@ dev = [
41
40
  'pytest-django~=4.7',
42
41
  'pytest-mock~=3.12',
43
42
  'coverage~=7.3',
44
- 'pre-commit~=3.5',
45
- 'ruff~=0.1.7',
43
+ 'pre-commit~=3.7',
44
+ 'ruff~=0.4',
46
45
  'sphinx~=7.1',
47
46
  'sphinx-rtd-theme~=2.0',
48
47
  'm2r2==0.3.3.post2',
49
48
  'mistune<2.0.0',
50
49
  'flit~=3.9',
51
- 'ambient-package-update~=24.6.4',
50
+ 'keyring~=25.2',
51
+ 'ambient-package-update',
52
52
  'unittest-parametrize~=1.3',
53
53
  ]
54
54
 
@@ -74,18 +74,24 @@ lint.select = [
74
74
  "DTZ", # flake8-datetimez
75
75
  "DJ", # flake8-django
76
76
  "TD", # flake8-to-do
77
- "RUF100", # Removes unnecessary "#noqa" comments
77
+ "RUF", # Ruff-specific rules
78
78
  "YTT", # Avoid non-future-prove usages of "sys"
79
- # "FBT", # Protects you from the "boolean trap bug"
80
79
  "C4", # Checks for unnecessary conversions
81
80
  "PIE", # Bunch of useful rules
82
- # "SIM", # Simplifies your code
83
81
  "INT", # Validates your gettext translation strings
84
82
  "PERF", # PerfLint
85
83
  "PGH", # No all-purpose "# noqa" and eval validation
86
84
  "PL", # PyLint
85
+ "LOG", # flake8-logging
86
+ "TID", # flake8-tidy-imports
87
+ "PLR2004", # Magic numbers
88
+ "BLE", # Checks for except clauses that catch all exceptions
89
+ "ANN401", # Checks that function arguments are annotated with a more specific type than Any
90
+ "TRY", # Clean try/except
91
+ "ERA", # Commented out code
87
92
  ]
88
93
  lint.ignore = [
94
+ 'TRY003', # Avoid specifying long messages outside the exception class
89
95
  ]
90
96
 
91
97
  # Allow autofix for all enabled rules (when `--fix`) is provided.
@@ -100,16 +106,21 @@ lint.fixable = [
100
106
  "DTZ", # flake8-datetimez
101
107
  "DJ", # flake8-django
102
108
  "TD", # flake8-to-do
103
- "RUF100", # Removes unnecessary "#noqa" comments
109
+ "RUF", # Ruff-specific rules
104
110
  "YTT", # Avoid non-future-prove usages of "sys"
105
- # "FBT", # Protects you from the "boolean trap bug"
106
111
  "C4", # Checks for unnecessary conversions
107
112
  "PIE", # Bunch of useful rules
108
- # "SIM", # Simplifies your code
109
113
  "INT", # Validates your gettext translation strings
110
114
  "PERF", # PerfLint
111
115
  "PGH", # No all-purpose "# noqa" and eval validation
112
116
  "PL", # PyLint
117
+ "LOG", # flake8-logging
118
+ "TID", # flake8-tidy-imports
119
+ "PLR2004", # Magic numbers
120
+ "BLE", # Checks for except clauses that catch all exceptions
121
+ "ANN401", # Checks that function arguments are annotated with a more specific type than Any
122
+ "TRY", # Clean try/except
123
+ "ERA", # Commented out code
113
124
  ]
114
125
  lint.unfixable = []
115
126
 
@@ -141,7 +152,7 @@ exclude = [
141
152
  line-length = 120
142
153
 
143
154
  # Allow unused variables when underscore-prefixed.
144
- dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
155
+ lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
145
156
 
146
157
  # Assume Python 3.12
147
158
  target-version = "py312"
@@ -161,10 +172,6 @@ line-ending = "auto"
161
172
 
162
173
  [tool.tox]
163
174
  legacy_tox_ini = """
164
- [tox]
165
- envlist = py{38,39,310,311}-django{32,41,42}
166
- isolated_build = True
167
-
168
175
  [testenv]
169
176
  # Django deprecation overview: https://www.djangoproject.com/download/
170
177
  deps =
@@ -176,7 +183,6 @@ commands =
176
183
 
177
184
  [gh-actions]
178
185
  python =
179
- 3.8: py38
180
186
  3.9: py39
181
187
  3.10: py310
182
188
  3.11: py311
@@ -1,7 +1,9 @@
1
1
  from logging import Logger
2
+ from threading import Thread
2
3
  from unittest import mock
4
+ from unittest.mock import Mock, call
3
5
 
4
- from django.test import TestCase
6
+ from django.test import TestCase, TransactionTestCase
5
7
  from django.utils import timezone
6
8
  from freezegun import freeze_time
7
9
 
@@ -81,3 +83,56 @@ class DatabasePreparationServiceTest(TestCase):
81
83
 
82
84
  self.assertEqual(calls[1].args, ("migrate",))
83
85
  self.assertEqual(calls[1].kwargs, {"check": True})
86
+
87
+
88
+ class DatabasePreparationServiceTestParallelDeployment(TransactionTestCase):
89
+ @mock.patch("django_migration_zero.services.deployment.get_logger")
90
+ def test_process_multiple_threads(self, mock_get_logger):
91
+ """Test parallel deployments to multiple pods."""
92
+ # Setup
93
+ mock_logger_info = Mock(return_value=None)
94
+ mock_get_logger.return_value = Mock(info=mock_logger_info)
95
+ config, _ = MigrationZeroConfiguration.objects.update_or_create(
96
+ defaults={
97
+ "migration_imminent": True,
98
+ "migration_date": timezone.now().date(),
99
+ }
100
+ )
101
+
102
+ # Testable
103
+ number_of_pods = 1
104
+ threads = [Thread(target=DatabasePreparationService().process) for _ in range(number_of_pods)]
105
+
106
+ for thread in threads:
107
+ thread.start()
108
+
109
+ for thread in threads:
110
+ thread.join()
111
+
112
+ # Assertions
113
+ self.assertEqual(mock_logger_info.call_count, 6 + number_of_pods, msg=mock_logger_info.call_args_list)
114
+ mock_logger_info.assert_has_calls(
115
+ [
116
+ call("Starting migration zero database adjustments..."),
117
+ call("Resetting migration history for all apps..."),
118
+ call("Populating migration history."),
119
+ call("Checking migration integrity."),
120
+ call("All good."),
121
+ call("Deactivating migration zero switch in database."),
122
+ call("Process successfully finished."),
123
+ ],
124
+ any_order=True,
125
+ )
126
+ self.assertEqual(
127
+ len(
128
+ [
129
+ mock_call
130
+ for mock_call in mock_logger_info.call_args_list
131
+ if mock_call == call("Starting migration zero database adjustments...")
132
+ ]
133
+ ),
134
+ number_of_pods,
135
+ )
136
+
137
+ config.refresh_from_db()
138
+ self.assertFalse(config.migration_imminent)
@@ -5,6 +5,7 @@ from django.db import ProgrammingError
5
5
  from django.test import TestCase
6
6
 
7
7
  from django_migration_zero.exceptions import MissingMigrationZeroConfigRecordError
8
+ from django_migration_zero.managers import MigrationZeroConfigurationManager
8
9
  from django_migration_zero.models import MigrationZeroConfiguration
9
10
 
10
11
 
@@ -18,7 +19,7 @@ class MigrationZeroConfigurationManagerTest(TestCase):
18
19
  def test_fetch_singleton_singleton_exists_via_migration(self):
19
20
  self.assertEqual(MigrationZeroConfiguration.objects.all().count(), 1)
20
21
 
21
- @mock.patch.object(MigrationZeroConfiguration.objects, "count", side_effect=ProgrammingError)
22
+ @mock.patch.object(MigrationZeroConfigurationManager, "select_for_update", side_effect=ProgrammingError)
22
23
  def test_fetch_singleton_database_error(self, *args):
23
24
  self.assertIsNone(MigrationZeroConfiguration.objects.fetch_singleton())
24
25