-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit ff0668f
Showing
18 changed files
with
477 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
syntax:glob | ||
*.py[co] | ||
dist/ | ||
*.egg-info/* | ||
.coverage | ||
.tox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
dist: bionic | ||
sudo: false | ||
language: python | ||
cache: pip | ||
python: | ||
- 3.6 | ||
- 3.7 | ||
- 3.8 | ||
stages: | ||
- lint | ||
- test | ||
jobs: | ||
fast_finish: true | ||
include: | ||
- { stage: lint, env: TOXENV=flake8, python: 3.6 } | ||
- { stage: lint, env: TOXENV=isort, python: 3.6 } | ||
- stage: deploy | ||
if: tag IS present | ||
python: 3.6 | ||
deploy: | ||
skip_existing: true | ||
provider: pypi | ||
user: charettes | ||
distributions: sdist bdist_wheel | ||
password: | ||
secure: P3/8C47HKEp0iQohltLjgFIK9jPGt5ZTeyNU9ayRa9J3C3yWgKGLJrTYNsxhoTT60ZGkLuOnpNioIDwZ3oBjm6OrONYZZ7xwXPxPaHa/Uxu91Bqkb/0IMSXWAKnjGCvE8DtDaDkBEOd52wyVinrR2YLp11oJR/cVUikFVFHpSBrxEl5SxgaulL61R4EmEVT1Y7y+dO5XqOBygb8/n2HWaCXw8Jd/gxgG/vYQy5zOC8S1sbRU6zR4DnpkDTDbLskuIDL/MySh8k793S/vqRWXeXMw8XGu6b5E3z69Yr7fB1UqUkwbr/GWnfkkjUyRxP8q8xk/5pvoJ1IUpLM9yFQTrOHtoJfFGaFpb/7kwuWdePL2+y5Er3YqNInrTvDg0yEyvGMPBLxv7D+tfpnNYoCvwfvspFkXLwl1w3CbM57FkPezHboEY+CPHWis1UnDNk/0g4Yn72NFcMcBHmnGOARrBXWEx345IU7QqJsFucFoYFLZHnYayiGu2S43j9UNk+2/5PUBve6Z0yQ5VKq4Obme9DS3LN3pbcBAoSpo5zszs2/xUaRO9KZ/7a6GScWCB9tmwFvfmsg5XAQQLD+aC49ZTjeFa3DeY/zufnah69eZgGcM1OKS8jOQrv9h5nAFvExV+gldAtVbC5FgEYTPn06E3oB0J3IMBqXEts5f9xzQSj0= | ||
on: | ||
tags: true | ||
|
||
install: | ||
- pip install tox coveralls tox-travis | ||
script: | ||
- tox | ||
after_success: | ||
- if [ -f .coverage ]; then coveralls; fi |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
Copyright (c) 2020, Simon Charette | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
django-syzygy | ||
============= | ||
|
||
.. image:: https://travis-ci.org/charettes/django-syzygy.svg?branch=master | ||
:target: https://travis-ci.org/charettes/django-syzygy | ||
:alt: Build Status | ||
|
||
.. image:: https://coveralls.io/repos/github/charettes/django-syzygy/badge.svg?branch=master | ||
:target: https://coveralls.io/github/charettes/django-syzygy?branch=master | ||
:alt: Coverage status | ||
|
||
|
||
Django application providing database migration tooling to automate their deployment. | ||
|
||
Installation | ||
------------ | ||
|
||
.. code:: sh | ||
pip install django-syzygy | ||
Concept | ||
------- | ||
|
||
Syzygy introduces a notion of _prerequisite_ and _postponed_ migrations with | ||
regards to deployment. | ||
|
||
A migration is assumed to be a _prerequisite_ unless it contains a destructive | ||
operation or the migration has its `postpone` class attribute set to `True`. | ||
When this boolean attribute is defined it will bypass `operations` based | ||
heuristics. | ||
|
||
e.g. this migration would be considered a _prerequisite_ | ||
|
||
.. code:: python | ||
class Migration(migrations.Migration): | ||
operations = [ | ||
AddField('model', 'field', models.IntegerField(null=True)) | ||
] | ||
while the following migrations would be _postponed_ | ||
|
||
.. code:: python | ||
class Migration(migrations.Migration): | ||
operations = [ | ||
RemoveField('model', 'field'), | ||
] | ||
In order to prevent the creation of migrations mixing operations of different | ||
nature this package registers system checks. These checks will generate an error | ||
for every migration not explicitly tagged using the `postpone` class attribute | ||
that contains an ambiguous sequence of operations. | ||
|
||
e.g. this migration would result in a check error | ||
|
||
.. code:: python | ||
class Migration(migrations.Migration): | ||
operations = [ | ||
AddField('model', 'other_field', models.IntegerField(null=True)), | ||
RemoveField('model', 'field'), | ||
] | ||
Migration revert are also supported and result in inverting the nature of | ||
migrations. A migration that is normally considered a _prerequisite_ would then | ||
be _postponed_ when reverted. | ||
|
||
Usage | ||
----- | ||
|
||
Add `'syzygy'` to your `INSTALLED_APPS` | ||
|
||
.. code:: python | ||
# settings.py | ||
INSTALLED_APPS = [ | ||
... | ||
'syzygy', | ||
... | ||
] | ||
Setup you deployment pipeline to run `migrate --prerequisite` before rolling | ||
out your code changes and `migrate` afterwards to apply the postponed | ||
migrations. | ||
|
||
Development | ||
----------- | ||
|
||
Make your changes, and then run tests via tox: | ||
|
||
.. code:: sh | ||
tox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[isort] | ||
combine_as_imports=true | ||
include_trailing_comma=true | ||
multi_line_output=5 | ||
not_skip=__init__.py | ||
|
||
[coverage:run] | ||
source = syzygy | ||
branch = 1 | ||
|
||
[flake8] | ||
max-line-length = 119 | ||
|
||
[wheel] | ||
universal = 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
#!/usr/bin/env python | ||
from __future__ import unicode_literals | ||
|
||
from setuptools import find_packages, setup | ||
|
||
with open("README.rst") as file_: | ||
long_description = file_.read() | ||
|
||
setup( | ||
name="django-syzygy", | ||
version="0.0.1", | ||
description="", | ||
long_description=long_description, | ||
url="https://github.com/charettes/django-syzygy", | ||
author="Simon Charette", | ||
author_email="charette.s@gmail.com", | ||
install_requires=["Django>=2.2"], | ||
packages=find_packages(exclude=["tests", "tests.*"]), | ||
license="MIT License", | ||
classifiers=[ | ||
"Environment :: Web Environment", | ||
"Framework :: Django", | ||
"Framework :: Django :: 2.2", | ||
"Framework :: Django :: 3.0", | ||
"Intended Audience :: Developers", | ||
"License :: OSI Approved :: MIT License", | ||
"Operating System :: OS Independent", | ||
"Programming Language :: Python", | ||
"Programming Language :: Python :: 3.6", | ||
"Programming Language :: Python :: 3.7", | ||
"Programming Language :: Python :: 3.8", | ||
"Topic :: Software Development :: Libraries :: Python Modules", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
default_app_config = "syzygy.apps.SyzygyConfig" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
from django.apps import AppConfig | ||
from django.core import checks | ||
|
||
from syzygy.checks import check_migrations | ||
|
||
|
||
class SyzygyConfig(AppConfig): | ||
name = __package__ | ||
|
||
def ready(self): | ||
checks.register(check_migrations, "migrations") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import os | ||
from importlib import import_module | ||
|
||
from django.apps import apps | ||
from django.core.checks import Error | ||
from django.db.migrations.loader import MigrationLoader | ||
from django.utils.module_loading import import_string | ||
|
||
from syzygy.plan import must_postpone_migration | ||
|
||
|
||
def check_migrations(app_configs, **kwargs): | ||
if app_configs is None: | ||
app_configs = apps.get_app_configs() | ||
errors = [] | ||
hint = "Assign `Migration.postpone` to denote whether or not the migration should be postponed." | ||
for app_config in app_configs: | ||
# Most of the following code is taken from MigrationLoader.load_disk | ||
# while allowing non-global app_configs to be used. | ||
module_name, _explicit = MigrationLoader.migrations_module(app_config.label) | ||
if module_name is None: | ||
continue | ||
try: | ||
module = import_module(module_name) | ||
except ImportError: | ||
# This is not the place to deal with migration issues. | ||
continue | ||
directory = os.path.dirname(module.__file__) | ||
migration_names = set() | ||
for name in os.listdir(directory): | ||
if name.endswith(".py"): | ||
import_name = name.rsplit(".", 1)[0] | ||
migration_names.add(import_name) | ||
for migration_name in migration_names: | ||
try: | ||
migration_class = import_string( | ||
f"{module_name}.{migration_name}.Migration" | ||
) | ||
except ImportError: | ||
# This is not the place to deal with migration issues. | ||
continue | ||
migration = migration_class(migration_name, app_config.label) | ||
try: | ||
must_postpone_migration(migration) | ||
except ValueError as e: | ||
errors.append( | ||
Error(str(e), hint=hint, obj=migration, id="migrations.0001") | ||
) | ||
return errors |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
from django.core.management import CommandError | ||
from django.core.management.commands.migrate import Command as MigrateCommand | ||
from django.db.models.signals import pre_migrate | ||
|
||
from syzygy.plan import get_prerequisite_plan | ||
|
||
|
||
class Command(MigrateCommand): | ||
def add_arguments(self, parser): | ||
super().add_arguments(parser) | ||
parser.add_argument("--prerequisite", action="store_true") | ||
|
||
def migrate_prerequisite(self, plan, **kwargs): | ||
try: | ||
plan[:] = get_prerequisite_plan(plan) | ||
except ValueError as exc: | ||
raise CommandError(str(exc)) from exc | ||
|
||
def handle(self, *args, prerequisite, **options): | ||
if prerequisite: | ||
pre_migrate.connect(self.migrate_prerequisite) | ||
super().handle(*args, **options) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
from typing import List, NewType, Optional, Tuple | ||
|
||
from django.conf import settings | ||
from django.db.migrations import DeleteModel, Migration, RemoveField | ||
from django.db.migrations.operations.base import Operation | ||
|
||
Plan = NewType("Plan", List[Tuple[Migration, bool]]) | ||
|
||
|
||
def must_postpone_operation( | ||
operation: Operation, backward: bool = False | ||
) -> Optional[bool]: | ||
"""Return whether not operation must be postponed.""" | ||
if isinstance(operation, (DeleteModel, RemoveField)): | ||
return not backward | ||
# All other operations are assumed to be prerequisite. | ||
return backward | ||
|
||
|
||
def must_postpone_migration( | ||
migration: Migration, backward: bool = False | ||
) -> Optional[bool]: | ||
""" | ||
Return whether or not migration must be postponed. | ||
If not specified through a `postpone` :class:`django.db.migrations.Migration` | ||
class attribute it will be tentatively deduced from its list of | ||
attr:`django.db.migrations.Migration.operations`. | ||
In cases of ambiguity a `ValueError` will be raised. | ||
""" | ||
# Postponed migrations are considered prerequisite when they are reverted. | ||
try: | ||
setting = settings.SYZYGY_POSTPONE | ||
except AttributeError: | ||
global_postpone = None | ||
else: | ||
key = (migration.app_label, migration.name) | ||
global_postpone = setting.get(key) | ||
postpone = getattr(migration, "postpone", global_postpone) | ||
if postpone is True: | ||
return not backward | ||
elif postpone is False: | ||
return backward | ||
# Migrations without operations such as merges are never postponed. | ||
if not migration.operations: | ||
return False | ||
for operation in migration.operations: | ||
postpone_operation = must_postpone_operation(operation, backward) | ||
if postpone is None: | ||
postpone = postpone_operation | ||
elif postpone_operation != postpone: | ||
raise ValueError( | ||
f"Cannot determine whether or not {migration} should be postponed." | ||
) | ||
return postpone | ||
|
||
|
||
def get_prerequisite_plan(plan: Plan) -> Plan: | ||
""" | ||
Trim provided plan to its leading contiguous prerequisite sequence. | ||
If the plan contains non-contiguous sequence of prerequisite migrations | ||
or migrations with ambiguous prerequisite nature a `ValueError` is raised. | ||
""" | ||
prerequisite_plan = [] | ||
postpone = False | ||
for migration, backward in plan: | ||
if must_postpone_migration(migration, backward): | ||
postpone = True | ||
else: | ||
if postpone: | ||
raise ValueError( | ||
"Plan contains a non-contiguous sequence of prerequisite " | ||
"migrations." | ||
) | ||
prerequisite_plan.append((migration, backward)) | ||
return prerequisite_plan |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
SECRET_KEY = "not-secret-anymore" | ||
|
||
TIME_ZONE = "America/Montreal" | ||
|
||
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3"}} | ||
|
||
INSTALLED_APPS = ["syzygy", "tests"] |
Oops, something went wrong.