How to set up and run migrations in third-party Django packages
Tags: django, python
Published: 2021-07-22 17:02:10

Creating a simple **python** package is a quite easy endeavor: create an empty project, create a module, put content inside, package it and done, you have a fully fledged package. However, creating a package that needs to integrate with **Django** is not that easy; you need to make sure that the models, migrations, default settings, etc... are in the correct place in order to flawlessly integrate without much pain.

This article will show you a way to run the Django migrations for your third party packages, decoupled from any external dependency. If any of you knows a simpler solution than the one exposed here, I will appreciate it so much!

*DISCLAIMER: This guide assumes that some concepts and terms related to Django like migrations and settings are known by the reader.*

## Pre-requisites

Django is a dependency of any third party django package, so you'll have to declare it as such (with poetry, pipenv...)

## The solution

You placed your Django Models in a `models.py`, but since you're implementing this third party library outside of the context of your monolith, there is no `manage.py` to be found. But you want to run your migrations!

Then you'll need:
* Create a python package folder called `migrations` inside your django package. Once you successfully run the migrations, they will be placed here.
* Create a `scripts` folder on the root of the project.
  * Inside this folder, create a `makemigrations.py` file. Put this content inside it. With that you'll be able to run the makemigrations script:

```python
import os
import sys

if __name__ == "__main__":
    os.environ.setdefault(
        "DJANGO_SETTINGS_MODULE", "scripts.mock_settings"
    )
    from django.core.management import execute_from_command_line

    args = sys.argv + ["makemigrations"]
    execute_from_command_line(args)
```
  * Add a `mock_settings.py` too. This file is necessary for the `makemigrations` command since any Django execution (and makemigrations is) requires a settings file. However, since we only need it to generate the migrations we can have a sample and dumb settings file just to do this operation. Put this content in the file:

```python
import os

SECRET_KEY = 'Not a secret key at all, actually'
DEBUG = True

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": "postgres",
        "USER": "postgres",
        "PASSWORD": "example",
        "HOST": "localhost",
        "PORT": "60000",
    }
}

INSTALLED_APPS = [
    # Django apps necessary to run the admin site
    'django.contrib.auth',
    'django.contrib.admin',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.staticfiles',
    'django.contrib.messages',

    # And this app
    'your_django_lib',
]

STATIC_URL = '/static/'
MIDDLEWARE = [
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'OPTIONS': {
            'context_processors': [
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        }
    },
]
```

  NOTE: Change this mock settings depending on your necessities. Instead of having to configure a real postgres database you could just change de `DATABASE` settings and configure a sqlite database. However sqlite does not support the likes of the JSONField, and migrations with JSONFields won't work.

   * If you need to spin up a real database just for the sake of migrations, you could use docker-compose. Use the following minimal setup:

```
version: '3.6'

services:
  postgres:
    image: postgres:9.5-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - 60000:5432
    expose:
      - "5432"
    restart: always
    environment:
      POSTGRES_PASSWORD: example

volumes:
  postgres_data:
```

In the end, the minimal structure for this to work would look something like this:
```
your-django-lib/
├─ your_django_lib/
│  ├─ migrations/
│  │  ├─ __init__.py
│  ├─ __init__.py
│  ├─ models.py
├─ scripts/
│  ├─ __init__.py
│  ├─ mock_settings.py
│  ├─ makemigrations.py
├─ .gitignore
├─ docker-compose.yml
```

Now you can run the `makemigrations` script. Depending on your virtual environment you'll have to execute the command in a different manner. With [poetry](https://python-poetry.org/) for example you'd run it like this:

```
poetry run python scripts/makemigrations.py
```

Congratulations! It's been tough, but now you can run your migrations decoupled from any infrastructure or external Django dependencies! Now you should have your model migrations in the migrations folder!