Skip to content

Commit

Permalink
Support after_deploy() migrations after the delay has passed (#53)
Browse files Browse the repository at this point in the history
* Convert Safe to a custom class rather than an enum.

This allows us to specify and store the delay associated
with after_deploy.

This also adds a check to determine if migrations are using
the enum style when it should be using the method definition.

* Render a new message for delayed after_deploy migrations.

* Support running the tests against multiple databases.

This likely needs work around the coverage aggregation.

* Add SafeMigration model with migration.

* Support Safe.after_deploy() with a delay.

* Limit mysql and postgres tests to python 3.10-3.12 as per tox limits

* Use future type annotations until 3.8 is retired.
  • Loading branch information
tim-schilling authored Jul 16, 2024
1 parent dcb1cae commit a41cebe
Show file tree
Hide file tree
Showing 13 changed files with 803 additions and 69 deletions.
156 changes: 154 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,156 @@ name: Build
on: push

jobs:
tests:
mysql:
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"

services:
mariadb:
image: mariadb
env:
MARIADB_ROOT_PASSWORD: django_safemigrate
options: >-
--health-cmd "mariadb-admin ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 3306:3306

runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache pip
uses: actions/cache@v3
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.python-version }}-v1-${{ hashFiles('**/pyproject.toml') }}-${{ hashFiles('**/tox.ini') }}
restore-keys: |
${{ matrix.python-version }}-v1-
- name: Install Tox
run: python -m pip install --upgrade pip tox tox-gh-actions coverage[toml]

- name: Run tox
env:
DB_BACKEND: mysql
DB_USER: root
DB_PASSWORD: django_safemigrate
DB_HOST: 127.0.0.1
DB_PORT: 3306
run: |
tox
python -m coverage combine
python -m coverage xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: Python ${{ matrix.python-version }} MySQL
files: "coverage.xml"
fail_ci_if_error: true
verbose: true


postgres:
strategy:
fail-fast: false
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
database: [postgresql]
# Add psycopg3 to our matrix for modern python versions
include:
- python-version: '3.10'
database: psycopg3
- python-version: '3.11'
database: psycopg3
- python-version: '3.12'
database: psycopg3

services:
postgres:
image: postgres
env:
POSTGRES_DB: django_safemigrate
POSTGRES_USER: django_safemigrate
POSTGRES_PASSWORD: django_safemigrate
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

- name: Get pip cache dir
id: pip-cache
run: |
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache pip
uses: actions/cache@v3
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
${{ matrix.python-version }}-v1-${{ hashFiles('**/pyproject.toml') }}-${{ hashFiles('**/tox.ini') }}
restore-keys: |
${{ matrix.python-version }}-v1-
- name: Install Tox
run: python -m pip install --upgrade pip tox tox-gh-actions coverage[toml]

- name: Run tox
env:
DB_BACKEND: ${{ matrix.database }}
DB_HOST: localhost
DB_PORT: 5432
run: |
tox
python -m coverage combine
python -m coverage xml
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: Python ${{ matrix.python-version }} ${{ matrix.database }}
files: "coverage.xml"
fail_ci_if_error: true
verbose: true


sqlite:
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -40,6 +189,9 @@ jobs:
run: python -m pip install --upgrade pip tox tox-gh-actions coverage[toml]

- name: Run tox
env:
DB_BACKEND: sqlite3
DB_NAME: ":memory:"
run: |
tox
python -m coverage combine
Expand All @@ -49,7 +201,7 @@ jobs:
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
name: Python ${{ matrix.python-version }}
name: Python ${{ matrix.python-version }} SQLite
files: "coverage.xml"
fail_ci_if_error: true
verbose: true
8 changes: 8 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ Pending

* Add support for Django 5.1.
* Drop support for Django 3.2, 4.0, 4.1.
* Convert ``Safe`` to be a custom class rather than an ``Enum``.
* The valid values for ``safe`` are:

* ``Safe.before_deploy()``
* ``Safe.after_deploy()``
* ``Safe.always()``
* Add support for allowing a ``Safe.after_deploy(delay=timedelta())``
migration to be migrated after the delay has passed.

4.3 (2024-03-28)
++++++++++++++++
Expand Down
22 changes: 18 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ such as a migration to add a column.
from django_safemigrate import Safe
class Migration(migrations.Migration):
safe = Safe.before_deploy
safe = Safe.before_deploy()
At this point you can run the ``safemigrate`` Django command
to run the migrations, and only these migrations will run.
Expand All @@ -66,18 +66,32 @@ Safety Options
There are three options for the value of the
``safe`` property of the migration.

* ``Safe.before_deploy``
* ``Safe.before_deploy()``

This migration is only safe to run before the code change is deployed.
For example, a migration that adds a new field to a model.

* ``Safe.after_deploy``
* ``Safe.after_deploy(delay=None)``

This migration is only safe to run after the code change is deployed.
This is the default that is applied if no ``safe`` property is given.
For example, a migration that removes a field from a model.

* ``Safe.always``
By specifying a ``delay`` parameter, you can specify when a
``Safe.after_deploy()`` migration can be run with the ``safemigrate``
command. For example, if it's desired to wait a week before applying
a migration, you can specify ``Safe.after_deploy(delay=timedelta(days=7))``.

The ``delay`` is used with the datetime when the migration is first detected.
The detection datetime is when the ``safemigrate`` command detects the
migration in a plan that successfully runs. If the migration plan is blocked,
such when a ``Safe.after_deploy(delay=None)`` is in front of a
``Safe.before_deploy()``, no migrations are marked as detected.

Note that a ``Safe.after_deploy()`` migration will not run the first
time it's encountered.

* ``Safe.always()``

This migration is safe to run before *and* after
the code change is deployed.
Expand Down
24 changes: 23 additions & 1 deletion src/django_safemigrate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta
from enum import Enum


class Safe(Enum):
class SafeEnum(Enum):
always = "always"
before_deploy = "before_deploy"
after_deploy = "after_deploy"


@dataclass
class Safe:
safe: SafeEnum
delay: timedelta | None = None

@classmethod
def always(cls):
return cls(safe=SafeEnum.always)

@classmethod
def before_deploy(cls):
return cls(safe=SafeEnum.before_deploy)

@classmethod
def after_deploy(cls, *, delay: timedelta = None):
return cls(safe=SafeEnum.after_deploy, delay=delay)
25 changes: 22 additions & 3 deletions src/django_safemigrate/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,25 @@
MISSING_SAFE_MESSAGE = (
"{file_path}: {migration_class} is missing the 'safe' attribute.\n"
)
UPGRADE_SAFE_DEFINITION_MESSAGE = "{file_path}: {migration_class} is using `{definition}` when it should be using `{corrected}`\n"
FAILURE_MESSAGE = (
"\n"
"Add the following to the migration class:\n"
"\n"
"from django_safemigrate import Safe\n"
"class Migration(migrations.Migration):\n"
" safe = Safe.before_deploy\n"
" safe = Safe.before_deploy()\n"
"\n"
"You can also use the following:\n"
" safe = Safe.always\n"
" safe = Safe.after_deploy\n"
" safe = Safe.always()\n"
" safe = Safe.after_deploy()\n"
"\n"
)
ENUM_DEFINITIONS = [
"safe = Safe.always\n",
"safe = Safe.after_deploy\n",
"safe = Safe.before_deploy\n",
]


def validate_migrations(files):
Expand All @@ -43,6 +49,19 @@ def validate_migrations(files):
file_path=file_path, migration_class=migration_class
)
)
for definition in ENUM_DEFINITIONS:
if definition in content:
definition = definition.replace("\n", "")
corrected = f"{definition}()"
success = False
sys.stdout.write(
UPGRADE_SAFE_DEFINITION_MESSAGE.format(
file_path=file_path,
migration_class=migration_class,
definition=definition,
corrected=corrected,
)
)
if not success:
sys.stdout.write(FAILURE_MESSAGE)
return success
Expand Down
Loading

0 comments on commit a41cebe

Please sign in to comment.