Skip to content

Commit

Permalink
Refactor how the initial .env is created
Browse files Browse the repository at this point in the history
  • Loading branch information
epicserve committed Sep 1, 2023
1 parent a9e4017 commit 5b45954
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 30 deletions.
120 changes: 120 additions & 0 deletions config/env_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional

import environs


class EnvWriter:
var_data: dict[str, Any]
write_dot_env_file: bool = False
base_dir: Path

def __init__(
self,
base_dir: Optional[Path] = None,
read_dot_env_file: bool = True,
eager: bool = True,
expand_vars: bool = False,
):
self.var_data = {}
self._env = environs.Env(eager=eager, expand_vars=expand_vars)
self.write_dot_env_file = self._env.bool("WRITE_DOT_ENV_FILE", default=False)
self._env = environs.Env(eager=eager, expand_vars=expand_vars)
self.base_dir = environs.Path(__file__).parent if base_dir is None else base_dir # type: ignore
if read_dot_env_file is True and self.write_dot_env_file is False:
self._env.read_env(str(self.base_dir.joinpath(".env")))

def _get_var(
self,
environs_instance,
var_type: str,
environ_args: tuple[Any, ...],
environ_kwargs: Optional[dict[str, Any]] = None,
):
environ_kwargs = environ_kwargs or {}
help_text = environ_kwargs.pop("help_text", None)
initial = environ_kwargs.pop("initial", None)

if self.write_dot_env_file is True:
self.var_data[environ_args[0]] = {
"type": var_type,
"default": environ_kwargs.get("default"),
"help_text": help_text,
"initial": initial,
}

try:
return getattr(environs_instance, var_type)(*environ_args, **environ_kwargs)
except environs.EnvError as e:
if self.write_dot_env_file is False:
raise e

def __call__(self, *args, **kwargs):
return self._get_var(self._env, var_type="str", environ_args=args, environ_kwargs=kwargs)

def __getattr__(self, item):
allowed_methods = [
"int",
"bool",
"str",
"float",
"decimal",
"list",
"dict",
"json",
"datetime",
"date",
"time",
"path",
"log_level",
"timedelta",
"uuid",
"url",
"enum",
"dj_db_url",
"dj_email_url",
"dj_cache_url",
]
if item not in allowed_methods:
return AttributeError(f"'{type(self).__name__}' object has no attribute '{item}'")

def _get_var(*args, **kwargs):
return self._get_var(self._env, var_type=item, environ_args=args, environ_kwargs=kwargs)

return _get_var

def write_env_file(self, env_file_path: Optional[Path] = None, overwrite_existing: bool = False):
if env_file_path is None:
env_file_path = self.base_dir.joinpath(".env")

if env_file_path.exists() is True and overwrite_existing is False:
backup_path = f"{env_file_path}.{datetime.now().strftime('%Y%m%d%H%M%S')}"
shutil.copy(env_file_path, backup_path)

with open(env_file_path, "w") as f:
env_str = (
f"# This is an initial .env file generated on {datetime.now(timezone.utc).isoformat()}. Any environment variable with a default\n" # noqa: E501
"# can be safely removed or commented out. Any variable without a default must be set.\n\n"
)
for key, data in self.var_data.items():
initial = data.get("initial", None)
val = ""

if data["help_text"] is not None:
env_str += f"# {data['help_text']}\n"
env_str += f"# type: {data['type']}\n"

if data["default"] is not None:
env_str += f"# default: {data['default']}\n"

if initial is not None and val == "":
val = initial()

if val == "" and data["default"] is not None:
env_str += f"# {key}={val}\n\n"
else:
env_str += f"{key}={val}\n\n"

f.write(env_str)
56 changes: 34 additions & 22 deletions config/settings/_base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import base64
import os
import socket

import environs

env = environs.Env()
from config.env_writer import EnvWriter

"""
Django settings for config project.
Expand All @@ -16,25 +17,28 @@

BASE_DIR = environs.Path(__file__).parent.parent.parent # type: ignore

READ_DOT_ENV_FILE = env.bool("READ_DOT_ENV_FILE", default=True)

if READ_DOT_ENV_FILE is True:
env.read_env(str(BASE_DIR.joinpath(".env")))
env = EnvWriter(base_dir=BASE_DIR, read_dot_env_file=False)

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env("SECRET_KEY")
SECRET_KEY = env(
"SECRET_KEY",
initial=lambda: base64.b64encode(os.urandom(60)).decode(),
help_text="Django's SECRET_KEY used to provide cryptographic signing.",
)

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool("DEBUG", default=False)
DEBUG = env.bool("DEBUG", default=True, help_text="Set Django Debug mode to on or off")

ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])
INTERNAL_IPS = env.list("INTERNAL_IPS", default=["127.0.0.1"])
ALLOWED_HOSTS = env.list(
"ALLOWED_HOSTS", default=["127.0.0.1"], help_text="List of allowed hosts that this Django site can serve"
)
INTERNAL_IPS = env.list("INTERNAL_IPS", default=["127.0.0.1"], help_text="IPs allowed to run in debug mode")

# Get the IP to use for Django Debug Toolbar when developing with docker
if env.bool("USE_DOCKER", default=False) is True:
if env.bool("USE_DOCKER", default=True, help_text="Used to set add the IP to INTERNAL_IPS for Docker Compose") is True:
ip = socket.gethostbyname(socket.gethostname())
INTERNAL_IPS += [ip[:-1] + "1"]

Expand Down Expand Up @@ -86,14 +90,15 @@
},
]

WSGI_APPLICATION = env("WSGI_APPLICATION", default="config.wsgi.application")
DB_SSL_REQUIRED = env.bool("DB_SSL_REQUIRED", default=not DEBUG)
WSGI_APPLICATION = env("WSGI_APPLICATION", default="config.wsgi.application", help_text="WSGI application to use")

# Database
# See https://github.com/jacobian/dj-database-url for more examples
DATABASES = {
"default": env.dj_db_url(
"DATABASE_URL", default=f'sqlite:///{BASE_DIR.joinpath("db.sqlite")}', ssl_require=DB_SSL_REQUIRED
"DATABASE_URL",
default="postgres://postgres:@db:5432/postgres",
help_text="Database URL for connecting to database",
)
}

Expand Down Expand Up @@ -143,7 +148,11 @@

STORAGES = {
"default": {
"BACKEND": env("DEFAULT_FILE_STORAGE", default="django.core.files.storage.FileSystemStorage"),
"BACKEND": env(
"DEFAULT_FILE_STORAGE",
default="django.core.files.storage.FileSystemStorage",
help_text="Default storage backend for media files",
),
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
Expand All @@ -153,11 +162,13 @@

if STORAGES["default"]["BACKEND"].endswith("MediaS3Storage") is True:
STORAGES["staticfiles"]["BACKEND"] = env("STATICFILES_STORAGE")
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = env("AWS_STORAGE_BUCKET_NAME")
AWS_ACCESS_KEY_ID = env("AWS_ACCESS_KEY_ID", help_text="AWS Access Key ID if using S3 storage backend")
AWS_SECRET_ACCESS_KEY = env("AWS_SECRET_ACCESS_KEY", help_text="AWS Secret Access Key if using S3 storage backend")
AWS_STORAGE_BUCKET_NAME = env(
"AWS_STORAGE_BUCKET_NAME", help_text="AWS Storage Bucket Name if using S3 storage backend"
)
AWS_DEFAULT_ACL = "public-read"
AWS_S3_REGION = env("AWS_S3_REGION", default="us-east-2")
AWS_S3_REGION = env("AWS_S3_REGION", default="us-east-2", help_text="AWS S3 Region if using S3 storage backend")
AWS_S3_CUSTOM_DOMAIN = f"s3.{AWS_S3_REGION}.amazonaws.com/{AWS_STORAGE_BUCKET_NAME}"
AWS_S3_OBJECT_PARAMETERS = {"CacheControl": "max-age=86400"}
STATIC_URL = f"https://{AWS_S3_CUSTOM_DOMAIN}/static/"
Expand All @@ -179,11 +190,11 @@
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

# CACHE SETTINGS
CACHE_URL = env("REDIS_URL", default="redis://redis:6379/0")
REDIS_URL = env("REDIS_URL", default="redis://redis:6379/0", help_text="Redis URL for connecting to redis")
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": CACHE_URL,
"LOCATION": REDIS_URL,
}
}

Expand All @@ -192,7 +203,7 @@
CRISPY_TEMPLATE_PACK = "bootstrap5"

# CELERY SETTINGS
CELERY_BROKER_URL = env("CACHE_URL", CACHE_URL)
CELERY_BROKER_URL = REDIS_URL

SESSION_ENGINE = "django.contrib.sessions.backends.cache"

Expand Down Expand Up @@ -227,6 +238,7 @@
email = env.dj_email_url(
"EMAIL_URL",
default="smtp://[email protected]:[email protected]:587/?ssl=True&_default_from_email=President%20Skroob%20%[email protected]%3E",
help_text="URL used for setting Django's email settings",
)
DEFAULT_FROM_EMAIL = email["DEFAULT_FROM_EMAIL"]
EMAIL_HOST = email["EMAIL_HOST"]
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ services:
- postgres_data:/var/lib/postgresql/data/
ports:
- "5432:5432"
env_file:
- .env
environment:
- POSTGRES_HOST_AUTH_METHOD=trust

Expand Down Expand Up @@ -45,6 +47,8 @@ services:
- db
- redis

env_file:
- .env
environment:
USE_DOCKER: 'on'
DJANGO_SETTINGS_MODULE: config.settings
Expand All @@ -62,6 +66,8 @@ services:
depends_on:
- web

env_file:
- .env
environment:
DJANGO_SETTINGS_MODULE: config.settings

Expand All @@ -83,6 +89,8 @@ services:
ports:
- "3000:3000"

env_file:
- .env
environment:
NODE_ENV: development

Expand Down
6 changes: 6 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ reset := `tput -Txterm sgr0`
@build_assets:
{{ node_cmd_prefix }} npm run build

# Create an initial .env file
@create_env_file:
# Create an empty .env so that docker-compose doesn't fail
touch .env;
{{ python_cmd_prefix }} ./scripts/create_initial_env.py

# Format SASS/CSS code
@format_sass:
just _start_msg "Formatting SASS code using stylelint"
Expand Down
13 changes: 13 additions & 0 deletions scripts/create_initial_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env python
import os

from django.core.management.color import make_style

os.environ.setdefault("WRITE_DOT_ENV_FILE", "True")

from config import settings # noqa: E402

style = make_style()

settings.env.write_env_file()
print(style.SUCCESS("Successfully created initial env file!"))
9 changes: 1 addition & 8 deletions scripts/start_new_project
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,8 @@ else
cd $PROJECT_DIRECTORY
fi

SECRET_KEY=$(python -c "import random; print(''.join(random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%^&*(-_=+)') for i in range(50)))")
cat > .env <<EOF
DEBUG=on
SECRET_KEY='$SECRET_KEY'
DATABASE_URL=postgres://postgres:@db:5432/postgres
INTERNAL_IPS=127.0.0.1,0.0.0.0
EOF

just remove_extra_files
just create_env_file
find ./public -name ".keep" | xargs rm -rf

echo ""
Expand Down

0 comments on commit 5b45954

Please sign in to comment.