TL; DR: хитрость заключается в том, чтобы изменить os.environment
перед импортом settings/base.py
в любой settings/<purpose>.py
, это значительно упростит вещи.
От одной мысли о всех этих переплетающихся файлах у меня болит голова.
Объединение, импорт (иногда условно), переопределение, исправление того, что уже было установлено в случае, если настройка DEBUG
изменилась позже.
Какой кошмар!
Через годы я прошел через все различные решения. Все они несколько работают, но им так больно управлять.
WTF! Нам действительно нужны все эти хлопоты? Мы начали с одного settings.py
файла.
Теперь нам нужна документация только для того, чтобы правильно объединить все это в правильном порядке!
Я надеюсь, что наконец-то достиг (моей) сладости с решением ниже.
Давайте вспомним цели (некоторые общие, некоторые мои)
Храните секреты в секрете - не храните их в репо.
Установка / чтение ключей и секретов с помощью настроек среды, 12-факторный стиль .
Имеют разумные запасные значения по умолчанию. В идеале для местного развития вам не нужно ничего, кроме значений по умолчанию.
… но старайтесь обеспечить безопасность производства по умолчанию. Лучше пропустить настройку локально,
чем необходимость помнить о необходимости настройки параметров по умолчанию, безопасных для производства.
Иметь возможность включать / выключать DEBUG
таким образом, чтобы это могло повлиять на другие настройки (например, с использованием сжатого JavaScript или нет).
Переключение между целевыми настройками, такими как локальные / тестирование / подготовка / производство, должно основываться только на DJANGO_SETTINGS_MODULE
, не более.
… но разрешить дальнейшую параметризацию с помощью настроек среды, таких как DATABASE_URL
.
… также позволяют им использовать различные настройки назначения и запускать их локально рядом, например. настройка производства на локальной машине разработчика для доступа к производственной базе данных или сжатым таблицам стилей для дымовых испытаний.
Ошибка, если переменная окружения не установлена явно (требуется как минимум пустое значение), особенно на производстве, например. EMAIL_HOST_PASSWORD
.
Ответ по умолчанию DJANGO_SETTINGS_MODULE
, заданный в manage.py во время django-admin startproject
Сохранение условий на минимуме, если условие предназначенный тип среды (например, для файла журнала производственных наборов и его ротации), переопределить настройки в связанном целевом файле настроек.
Не надо
Не позволяйте django читать настройки DJANGO_SETTINGS_MODULE из файла.
Тьфу! Подумайте, как это мета. Если вам нужен файл (например, докер
env) прочитайте это в среду перед запуском процесса django.
Не переопределяйте DJANGO_SETTINGS_MODULE в коде вашего проекта / приложения, например. на основе имени хоста или имени процесса.
Если вам лень устанавливать переменные среды (например, для setup.py test
), сделайте это в инструментах непосредственно перед запуском кода проекта.
Избегайте магии и исправлений того, как django читает свои настройки, предварительно обрабатывайте настройки, но не вмешивайтесь впоследствии.
Нет сложной логики, основанной на глупости. Конфигурация должна быть фиксированной и материализованной, а не вычисленной на лету.
Предоставление запасных значений по умолчанию здесь достаточно логики.
Вы действительно хотите отладить, почему локально у вас есть правильный набор настроек, но на производстве на удаленном сервере,
на одной из ста машин что-то вычисляется по-другому? Ой! Модульные тесты? Для настроек? Шутки в сторону?
Решение
Моя стратегия состоит из превосходных django-environment , используемых с файлами стиля ini
,
os.environment
по умолчанию для локальной разработки, некоторые минимальные и короткие settings/<purpose>.py
файлы, которые имеют
import settings/base.py
ПОСЛЕ os.environment
был установлен из файла INI
. Это эффективно дает нам своего рода инъекцию настроек.
Хитрость в том, чтобы изменить os.environment
перед импортом settings/base.py
.
Дляполный пример см. в репо: https://github.com/wooyek/django-settings-strategy
.
│ manage.py
├───data
└───website
├───settings
│ │ __init__.py <-- imports local for compatybility
│ │ base.py <-- almost all the settings, reads from proces environment
│ │ local.py <-- a few modifications for local development
│ │ production.py <-- ideally is empy and everything is in base
│ │ testing.py <-- mimics production with a reasonable exeptions
│ │ .env <-- for local use, not kept in repo
│ __init__.py
│ urls.py
│ wsgi.py
settings / .env
По умолчанию для локальной разработки.Секретный файл, в основном для установки необходимых переменных среды.Установите для них пустые значения, если они не требуются при локальной разработке.Здесь мы предоставляем значения по умолчанию, а не в settings/base.py
для сбоя на любом другом компьютере, если они отсутствуют в среде.
settings / local.py
Что происходит здесь, это загрузка средыиз settings/.env
, затем импортируя общие настройки из settings/base.py
.После этого мы можем переопределить несколько, чтобы упростить локальную разработку.
import logging
import environ
logging.debug("Settings loading: %s" % __file__)
# This will read missing environment variables from a file
# We wan to do this before loading a base settings as they may depend on environment
environ.Env.read_env(DEBUG='True')
from .base import *
ALLOWED_HOSTS += [
'127.0.0.1',
'localhost',
'.example.com',
'vagrant',
]
# https://docs.djangoproject.com/en/1.6/topics/email/#console-backend
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# EMAIL_BACKEND = 'django.core.mail.backends.dummy.EmailBackend'
LOGGING['handlers']['mail_admins']['email_backend'] = 'django.core.mail.backends.dummy.EmailBackend'
# Sync task testing
# http://docs.celeryproject.org/en/2.5/configuration.html?highlight=celery_always_eager#celery-always-eager
CELERY_ALWAYS_EAGER = True
CELERY_EAGER_PROPAGATES_EXCEPTIONS = True
settings / production.py
Для производства нам не следует ожидать файл среды, но его проще получить, если мыТестирую что-то.Но в любом случае, чтобы не обеспечить несколько встроенных значений по умолчанию, поэтому settings/base.py
может ответить соответственно.
environ.Env.read_env(Path(__file__) / "production.env", DEBUG='False', ASSETS_DEBUG='False')
from .base import *
Основными интересными моментами здесь являются переопределения DEBUG
и ASSETS_DEBUG
, они будут применяться ТОЛЬКО к питону os.environ
, если они ПРОПУСКАЮТСЯ из среды и файла.
Это будут наши производственные настройки по умолчанию, нет необходимости помещать их в среду или файл, но они могут быть переопределены при необходимости.Neat!
settings / base.py
Это ваши в основном ванильные настройки django, с несколькими условными выражениями и множеством чтений из среды.Здесь есть почти все, поддерживая согласованность и максимально возможную схожесть всех целевых сред.
Основные различия приведены ниже (я надеюсь, они говорят сами за себя):
import environ
# https://github.com/joke2k/django-environ
env = environ.Env()
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Where BASE_DIR is a django source root, ROOT_DIR is a whole project root
# It may differ BASE_DIR for eg. when your django project code is in `src` folder
# This may help to separate python modules and *django apps* from other stuff
# like documentation, fixtures, docker settings
ROOT_DIR = BASE_DIR
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG', default=False)
INTERNAL_IPS = [
'127.0.0.1',
]
ALLOWED_HOSTS = []
if 'ALLOWED_HOSTS' in os.environ:
hosts = os.environ['ALLOWED_HOSTS'].split(" ")
BASE_URL = "https://" + hosts[0]
for host in hosts:
host = host.strip()
if host:
ALLOWED_HOSTS.append(host)
SECURE_SSL_REDIRECT = env.bool('SECURE_SSL_REDIRECT', default=False)
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
if "DATABASE_URL" in os.environ: # pragma: no cover
# Enable database config through environment
DATABASES = {
# Raises ImproperlyConfigured exception if DATABASE_URL not in os.environ
'default': env.db(),
}
# Make sure we use have all settings we need
# DATABASES['default']['ENGINE'] = 'django.contrib.gis.db.backends.postgis'
DATABASES['default']['TEST'] = {'NAME': os.environ.get("DATABASE_TEST_NAME", None)}
DATABASES['default']['OPTIONS'] = {
'options': '-c search_path=gis,public,pg_catalog',
'sslmode': 'require',
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
# 'ENGINE': 'django.contrib.gis.db.backends.spatialite',
'NAME': os.path.join(ROOT_DIR, 'data', 'db.dev.sqlite3'),
'TEST': {
'NAME': os.path.join(ROOT_DIR, 'data', 'db.test.sqlite3'),
}
}
}
STATIC_ROOT = os.path.join(ROOT_DIR, 'static')
# django-assets
# http://django-assets.readthedocs.org/en/latest/settings.html
ASSETS_LOAD_PATH = STATIC_ROOT
ASSETS_ROOT = os.path.join(ROOT_DIR, 'assets', "compressed")
ASSETS_DEBUG = env('ASSETS_DEBUG', default=DEBUG) # Disable when testing compressed file in DEBUG mode
if ASSETS_DEBUG:
ASSETS_URL = STATIC_URL
ASSETS_MANIFEST = "json:{}".format(os.path.join(ASSETS_ROOT, "manifest.json"))
else:
ASSETS_URL = STATIC_URL + "assets/compressed/"
ASSETS_MANIFEST = "json:{}".format(os.path.join(STATIC_ROOT, 'assets', "compressed", "manifest.json"))
ASSETS_AUTO_BUILD = ASSETS_DEBUG
ASSETS_MODULES = ('website.assets',)
Последний бит показывает мощность здесь.ASSETS_DEBUG
имеет разумное значение по умолчанию, которое может быть переопределено в settings/production.py
и даже то, что может быть переопределено настройкой среды!Ура!
По сути, мы имеем смешанную иерархию важности:
- settings / .py - устанавливает значения по умолчанию в зависимости от цели, не хранит секреты
- settings / base.py - в основном управляется средой
- настройки среды процесса - 12 фактор baby!
- settings / .env - локальные настройки по умолчанию для простого запуска