codefortify-starter 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. codefortify_starter/__init__.py +7 -0
  2. codefortify_starter/__main__.py +6 -0
  3. codefortify_starter/cli.py +103 -0
  4. codefortify_starter/constants.py +48 -0
  5. codefortify_starter/generator.py +246 -0
  6. codefortify_starter/template_engine.py +63 -0
  7. codefortify_starter/templates/base_project/README.md.jinja +87 -0
  8. codefortify_starter/templates/base_project/apps/__init__.py +1 -0
  9. codefortify_starter/templates/base_project/apps/home/__init__.py +1 -0
  10. codefortify_starter/templates/base_project/apps/home/apps.py.jinja +7 -0
  11. codefortify_starter/templates/base_project/apps/home/templates/home/index.html.jinja +25 -0
  12. codefortify_starter/templates/base_project/apps/home/tests.py.jinja +17 -0
  13. codefortify_starter/templates/base_project/apps/home/urls.py.jinja +14 -0
  14. codefortify_starter/templates/base_project/apps/home/views.py.jinja +13 -0
  15. codefortify_starter/templates/base_project/core/__init__.py.jinja +7 -0
  16. codefortify_starter/templates/base_project/core/asgi.py.jinja +10 -0
  17. codefortify_starter/templates/base_project/core/env.py.jinja +46 -0
  18. codefortify_starter/templates/base_project/core/middleware/__init__.py +1 -0
  19. codefortify_starter/templates/base_project/core/middleware/exceptions.py +26 -0
  20. codefortify_starter/templates/base_project/core/middleware/security.py +12 -0
  21. codefortify_starter/templates/base_project/core/settings/__init__.py +1 -0
  22. codefortify_starter/templates/base_project/core/settings/base.py.jinja +160 -0
  23. codefortify_starter/templates/base_project/core/settings/dev.py.jinja +8 -0
  24. codefortify_starter/templates/base_project/core/settings/production.py.jinja +21 -0
  25. codefortify_starter/templates/base_project/core/templates/errors/400.html +8 -0
  26. codefortify_starter/templates/base_project/core/templates/errors/403.html +8 -0
  27. codefortify_starter/templates/base_project/core/templates/errors/404.html +8 -0
  28. codefortify_starter/templates/base_project/core/templates/errors/405.html +8 -0
  29. codefortify_starter/templates/base_project/core/templates/errors/500.html +8 -0
  30. codefortify_starter/templates/base_project/core/urls.py.jinja +23 -0
  31. codefortify_starter/templates/base_project/core/views.py.jinja +56 -0
  32. codefortify_starter/templates/base_project/core/wsgi.py.jinja +10 -0
  33. codefortify_starter/templates/base_project/dot-env.example.jinja +66 -0
  34. codefortify_starter/templates/base_project/dot-gitignore +34 -0
  35. codefortify_starter/templates/base_project/manage.py.jinja +16 -0
  36. codefortify_starter/templates/base_project/static/css/main.css +19 -0
  37. codefortify_starter/templates/base_project/templates/base.html.jinja +19 -0
  38. codefortify_starter/templates/features/celery/apps/home/tasks.py.jinja +7 -0
  39. codefortify_starter/templates/features/celery/core/celery.py.jinja +16 -0
  40. codefortify_starter/templates/features/docker/DOCKER_README.md.jinja +67 -0
  41. codefortify_starter/templates/features/docker/Dockerfile.jinja +20 -0
  42. codefortify_starter/templates/features/docker/docker-compose.prod.yml.jinja +158 -0
  43. codefortify_starter/templates/features/docker/docker-compose.yml.jinja +208 -0
  44. codefortify_starter/templates/features/docker/docker_deploy.sh.jinja +132 -0
  45. codefortify_starter/templates/features/docker/dot-dockerignore +12 -0
  46. codefortify_starter/templates/features/docker/entrypoint.sh.jinja +95 -0
  47. codefortify_starter/templates/features/drf/apps/api/__init__.py +1 -0
  48. codefortify_starter/templates/features/drf/apps/api/apps.py.jinja +7 -0
  49. codefortify_starter/templates/features/drf/apps/api/serializers.py +7 -0
  50. codefortify_starter/templates/features/drf/apps/api/tests.py +11 -0
  51. codefortify_starter/templates/features/drf/apps/api/urls.py +11 -0
  52. codefortify_starter/templates/features/drf/apps/api/views.py +16 -0
  53. codefortify_starter/templates/features/htmx/templates/partials/example.html.jinja +5 -0
  54. codefortify_starter/validators.py +70 -0
  55. codefortify_starter-1.0.0.dist-info/METADATA +276 -0
  56. codefortify_starter-1.0.0.dist-info/RECORD +60 -0
  57. codefortify_starter-1.0.0.dist-info/WHEEL +5 -0
  58. codefortify_starter-1.0.0.dist-info/entry_points.txt +2 -0
  59. codefortify_starter-1.0.0.dist-info/licenses/LICENSE +22 -0
  60. codefortify_starter-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,46 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ ROOT_DIR = Path(__file__).resolve().parent.parent
6
+ DOTENV_FILE = ROOT_DIR / ".env"
7
+ DOTENV_LOCAL_FILE = ROOT_DIR / ".env.local"
8
+ TRUE_VALUES = {"1", "true", "yes", "on"}
9
+
10
+
11
+ def env_flag(name: str, default: bool = False) -> bool:
12
+ value = os.environ.get(name)
13
+ if value is None:
14
+ return default
15
+ return value.strip().lower() in TRUE_VALUES
16
+
17
+
18
+ def _load_env_file(path: Path) -> None:
19
+ if not path.exists():
20
+ return
21
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
22
+ line = raw_line.strip()
23
+ if not line or line.startswith("#") or "=" not in line:
24
+ continue
25
+ key, value = line.split("=", 1)
26
+ key = key.strip()
27
+ value = value.strip().strip('"').strip("'")
28
+ if key and key not in os.environ:
29
+ os.environ[key] = value
30
+
31
+
32
+ def configure_environment() -> str:
33
+ explicit_environment = (os.environ.get("CODEFORTIFY_ENVIRONMENT") or "").strip().lower()
34
+ load_dotenv_in_production = env_flag("CODEFORTIFY_LOAD_DOTENV_IN_PRODUCTION", default=False)
35
+ should_load_dotenv = explicit_environment != "production" or load_dotenv_in_production
36
+
37
+ if should_load_dotenv:
38
+ if DOTENV_LOCAL_FILE.exists() and not env_flag("CODEFORTIFY_SKIP_DOTENV_LOCAL", default=False):
39
+ _load_env_file(DOTENV_LOCAL_FILE)
40
+ _load_env_file(DOTENV_FILE)
41
+
42
+ environment = (os.environ.get("CODEFORTIFY_ENVIRONMENT") or "dev").strip().lower()
43
+ settings_module = "core.settings.production" if environment == "production" else "core.settings.dev"
44
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module)
45
+ return settings_module
46
+
@@ -0,0 +1,26 @@
1
+ import logging
2
+
3
+ from django.conf import settings
4
+ from django.http import JsonResponse
5
+
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ class SafeExceptionMiddleware:
11
+ def __init__(self, get_response):
12
+ self.get_response = get_response
13
+
14
+ def __call__(self, request):
15
+ try:
16
+ return self.get_response(request)
17
+ except Exception as exc: # pragma: no cover
18
+ logger.exception("Unhandled application exception")
19
+ if settings.DEBUG:
20
+ raise
21
+
22
+ accept_header = request.headers.get("Accept", "")
23
+ if "application/json" in accept_header or request.path.startswith("/api/"):
24
+ return JsonResponse({"detail": str(exc) or "Internal server error."}, status=500)
25
+ raise
26
+
@@ -0,0 +1,12 @@
1
+ class SecurityHeadersMiddleware:
2
+ def __init__(self, get_response):
3
+ self.get_response = get_response
4
+
5
+ def __call__(self, request):
6
+ response = self.get_response(request)
7
+ response.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
8
+ response.setdefault("X-Content-Type-Options", "nosniff")
9
+ response.setdefault("X-Frame-Options", "DENY")
10
+ response.setdefault("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
11
+ return response
12
+
@@ -0,0 +1,160 @@
1
+ from pathlib import Path
2
+
3
+ from decouple import Csv, config
4
+ from django.core.exceptions import ImproperlyConfigured
5
+ [% if use_postgres or use_mysql %]
6
+ import dj_database_url
7
+ [% endif %]
8
+ [% if use_mysql %]
9
+ import pymysql
10
+
11
+ pymysql.install_as_MySQLdb()
12
+ [% endif %]
13
+
14
+
15
+ BASE_DIR = Path(__file__).resolve().parent.parent.parent
16
+
17
+
18
+ DEBUG = config("DEBUG", default=True, cast=bool)
19
+ SECRET_KEY = config("SECRET_KEY", default="django-insecure-change-me")
20
+ if not DEBUG and SECRET_KEY == "django-insecure-change-me":
21
+ raise ImproperlyConfigured("SECRET_KEY must be set when DEBUG is False.")
22
+
23
+ ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost,127.0.0.1,0.0.0.0", cast=Csv())
24
+ CSRF_TRUSTED_ORIGINS = config(
25
+ "CSRF_TRUSTED_ORIGINS",
26
+ default="http://localhost:8000,http://127.0.0.1:8000",
27
+ cast=Csv(),
28
+ )
29
+
30
+ INSTALLED_APPS = [
31
+ "django.contrib.admin",
32
+ "django.contrib.auth",
33
+ "django.contrib.contenttypes",
34
+ "django.contrib.sessions",
35
+ "django.contrib.messages",
36
+ "django.contrib.staticfiles",
37
+ [% if use_htmx %]
38
+ "django_htmx",
39
+ [% endif %]
40
+ [% if use_drf %]
41
+ "rest_framework",
42
+ "django_filters",
43
+ "apps.api",
44
+ [% endif %]
45
+ "apps.home",
46
+ ]
47
+
48
+ MIDDLEWARE = [
49
+ "core.middleware.security.SecurityHeadersMiddleware",
50
+ "django.middleware.security.SecurityMiddleware",
51
+ [% if use_docker %]
52
+ "whitenoise.middleware.WhiteNoiseMiddleware",
53
+ [% endif %]
54
+ "django.contrib.sessions.middleware.SessionMiddleware",
55
+ "django.middleware.common.CommonMiddleware",
56
+ [% if use_htmx %]
57
+ "django_htmx.middleware.HtmxMiddleware",
58
+ [% endif %]
59
+ "django.middleware.csrf.CsrfViewMiddleware",
60
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
61
+ "django.contrib.messages.middleware.MessageMiddleware",
62
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
63
+ "core.middleware.exceptions.SafeExceptionMiddleware",
64
+ ]
65
+
66
+ ROOT_URLCONF = "core.urls"
67
+ WSGI_APPLICATION = "core.wsgi.application"
68
+ ASGI_APPLICATION = "core.asgi.application"
69
+
70
+ TEMPLATES = [
71
+ {
72
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
73
+ "DIRS": [BASE_DIR / "templates"],
74
+ "APP_DIRS": True,
75
+ "OPTIONS": {
76
+ "context_processors": [
77
+ "django.template.context_processors.request",
78
+ "django.contrib.auth.context_processors.auth",
79
+ "django.contrib.messages.context_processors.messages",
80
+ ]
81
+ },
82
+ }
83
+ ]
84
+
85
+ [% if use_sqlite %]
86
+ DATABASES = {
87
+ "default": {
88
+ "ENGINE": "django.db.backends.sqlite3",
89
+ "NAME": BASE_DIR / "db.sqlite3",
90
+ }
91
+ }
92
+ [% elif use_postgres or use_mysql %]
93
+ DATABASE_URL = config("DATABASE_URL", default="").strip()
94
+
95
+ if DATABASE_URL:
96
+ DATABASES = {
97
+ "default": dj_database_url.parse(
98
+ DATABASE_URL,
99
+ conn_max_age=config("DB_CONN_MAX_AGE", default=600, cast=int),
100
+ )
101
+ }
102
+ else:
103
+ DATABASES = {
104
+ "default": {
105
+ "ENGINE": "django.db.backends.sqlite3",
106
+ "NAME": BASE_DIR / "db.sqlite3",
107
+ }
108
+ }
109
+ [% endif %]
110
+
111
+ AUTH_PASSWORD_VALIDATORS = [
112
+ {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
113
+ {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
114
+ {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
115
+ {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
116
+ ]
117
+
118
+ LANGUAGE_CODE = config("LANGUAGE_CODE", default="en-us")
119
+ TIME_ZONE = config("TIME_ZONE", default="UTC")
120
+ USE_I18N = True
121
+ USE_TZ = True
122
+
123
+ STATIC_URL = "/static/"
124
+ STATIC_ROOT = BASE_DIR / "static_root"
125
+ STATICFILES_DIRS = [BASE_DIR / "static"]
126
+ [% if use_docker %]
127
+ STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
128
+ [% endif %]
129
+
130
+ MEDIA_URL = "/media/"
131
+ MEDIA_ROOT = BASE_DIR / "media"
132
+
133
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
134
+
135
+ [% if use_drf %]
136
+ REST_FRAMEWORK = {
137
+ "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.AllowAny",),
138
+ "DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",),
139
+ "DEFAULT_FILTER_BACKENDS": (
140
+ "django_filters.rest_framework.DjangoFilterBackend",
141
+ "rest_framework.filters.OrderingFilter",
142
+ "rest_framework.filters.SearchFilter",
143
+ ),
144
+ }
145
+ [% endif %]
146
+
147
+ [% if use_celery %]
148
+ REDIS_URL = config("REDIS_URL", default="redis://127.0.0.1:6379/0")
149
+ CELERY_BROKER_URL = config("CELERY_BROKER_URL", default=REDIS_URL)
150
+ CELERY_RESULT_BACKEND = config("CELERY_RESULT_BACKEND", default="redis://127.0.0.1:6379/1")
151
+ CELERY_ACCEPT_CONTENT = ["json"]
152
+ CELERY_TASK_SERIALIZER = "json"
153
+ CELERY_RESULT_SERIALIZER = "json"
154
+ CELERY_TIMEZONE = TIME_ZONE
155
+ CELERY_ENABLE_UTC = USE_TZ
156
+ [% endif %]
157
+
158
+ SESSION_COOKIE_HTTPONLY = True
159
+ SESSION_COOKIE_SECURE = config("SESSION_COOKIE_SECURE", default=not DEBUG, cast=bool)
160
+ CSRF_COOKIE_SECURE = config("CSRF_COOKIE_SECURE", default=not DEBUG, cast=bool)
@@ -0,0 +1,8 @@
1
+ from .base import * # noqa: F403,F401
2
+
3
+
4
+ DEBUG = True
5
+ SECURE_SSL_REDIRECT = False
6
+ SESSION_COOKIE_SECURE = False
7
+ CSRF_COOKIE_SECURE = False
8
+
@@ -0,0 +1,21 @@
1
+ from decouple import config
2
+ from django.core.exceptions import ImproperlyConfigured
3
+
4
+ from .base import * # noqa: F403,F401
5
+
6
+
7
+ DEBUG = False
8
+
9
+ if SECRET_KEY == "django-insecure-change-me": # noqa: F405
10
+ raise ImproperlyConfigured("SECRET_KEY must be set in production.") # noqa: F405
11
+
12
+ if not ALLOWED_HOSTS: # noqa: F405
13
+ raise ImproperlyConfigured("ALLOWED_HOSTS must be configured in production.") # noqa: F405
14
+
15
+ SECURE_SSL_REDIRECT = config("SECURE_SSL_REDIRECT", default=True, cast=bool)
16
+ SESSION_COOKIE_SECURE = True
17
+ CSRF_COOKIE_SECURE = True
18
+ SECURE_HSTS_SECONDS = config("SECURE_HSTS_SECONDS", default=31536000, cast=int)
19
+ SECURE_HSTS_INCLUDE_SUBDOMAINS = config("SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True, cast=bool)
20
+ SECURE_HSTS_PRELOAD = config("SECURE_HSTS_PRELOAD", default=True, cast=bool)
21
+ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
@@ -0,0 +1,8 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}400 Bad Request{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>400 - Bad Request</h1>
7
+ {% endblock %}
8
+
@@ -0,0 +1,8 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}403 Permission Denied{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>403 - Permission denied</h1>
7
+ {% endblock %}
8
+
@@ -0,0 +1,8 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}404 Not Found{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>404 - Page not found</h1>
7
+ {% endblock %}
8
+
@@ -0,0 +1,8 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}405 Method Not Allowed{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>405 - Method not allowed</h1>
7
+ {% endblock %}
8
+
@@ -0,0 +1,8 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}500 Server Error{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>500 - Internal server error</h1>
7
+ {% endblock %}
8
+
@@ -0,0 +1,23 @@
1
+ from django.conf import settings
2
+ from django.conf.urls.static import static
3
+ from django.contrib import admin
4
+ from django.urls import include, path
5
+
6
+
7
+ urlpatterns = [
8
+ path("admin/", admin.site.urls),
9
+ path("", include(("apps.home.urls", "home"), namespace="home")),
10
+ [% if use_drf %]
11
+ path("api/", include(("apps.api.urls", "api"), namespace="api")),
12
+ [% endif %]
13
+ ]
14
+
15
+ if settings.DEBUG:
16
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
17
+
18
+ handler400 = "core.views.bad_request"
19
+ handler403 = "core.views.permission_denied"
20
+ handler404 = "core.views.page_not_found"
21
+ handler405 = "core.views.method_not_allowed"
22
+ handler500 = "core.views.server_error"
23
+
@@ -0,0 +1,56 @@
1
+ from django.http import JsonResponse
2
+ from django.shortcuts import render
3
+ from django.views.decorators.csrf import requires_csrf_token
4
+
5
+
6
+ def _expects_json(request) -> bool:
7
+ accept_header = request.headers.get("Accept", "")
8
+ content_type = request.headers.get("Content-Type", "")
9
+ return (
10
+ "application/json" in accept_header
11
+ or "application/json" in content_type
12
+ or request.path.startswith("/api/")
13
+ )
14
+
15
+
16
+ @requires_csrf_token
17
+ def bad_request(request, exception, template_name="errors/400.html"):
18
+ if _expects_json(request):
19
+ return JsonResponse({"detail": "Bad request."}, status=400)
20
+ return render(request, template_name, status=400)
21
+
22
+
23
+ @requires_csrf_token
24
+ def permission_denied(request, exception, template_name="errors/403.html"):
25
+ if _expects_json(request):
26
+ return JsonResponse({"detail": "Permission denied."}, status=403)
27
+ return render(request, template_name, status=403)
28
+
29
+
30
+ @requires_csrf_token
31
+ def page_not_found(request, exception, template_name="errors/404.html"):
32
+ if _expects_json(request):
33
+ return JsonResponse({"detail": "Not found."}, status=404)
34
+ return render(request, template_name, status=404)
35
+
36
+
37
+ @requires_csrf_token
38
+ def method_not_allowed(request, exception, template_name="errors/405.html"):
39
+ if _expects_json(request):
40
+ return JsonResponse({"detail": "Method not allowed."}, status=405)
41
+ return render(request, template_name, status=405)
42
+
43
+
44
+ @requires_csrf_token
45
+ def server_error(request, template_name="errors/500.html"):
46
+ if _expects_json(request):
47
+ return JsonResponse({"detail": "Internal server error."}, status=500)
48
+ return render(request, template_name, status=500)
49
+
50
+
51
+ @requires_csrf_token
52
+ def csrf_failure(request, reason="", template_name="errors/403.html"):
53
+ if _expects_json(request):
54
+ return JsonResponse({"detail": "CSRF verification failed."}, status=403)
55
+ return render(request, template_name, status=403)
56
+
@@ -0,0 +1,10 @@
1
+ from core.env import configure_environment
2
+
3
+
4
+ configure_environment()
5
+
6
+ from django.core.wsgi import get_wsgi_application
7
+
8
+
9
+ application = get_wsgi_application()
10
+
@@ -0,0 +1,66 @@
1
+ # ------------------------------------------------------------------------------
2
+ # Runtime
3
+ # ------------------------------------------------------------------------------
4
+ CODEFORTIFY_ENVIRONMENT=dev
5
+ DEBUG=True
6
+ SECRET_KEY=change-me
7
+ ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
8
+ CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
9
+ TIME_ZONE=UTC
10
+ LANGUAGE_CODE=en-us
11
+
12
+ # ------------------------------------------------------------------------------
13
+ # Database
14
+ # ------------------------------------------------------------------------------
15
+ [% if use_sqlite %]
16
+ DATABASE_URL=
17
+ [% elif use_postgres %]
18
+ [% if use_docker %]
19
+ # Leave blank for local SQLite fallback.
20
+ # DATABASE_URL=sqlite:///db.sqlite3
21
+ # Docker PostgreSQL example:
22
+ # DATABASE_URL=postgres://[[ project_slug ]]:[[ project_slug ]]@db:5432/[[ project_slug ]]
23
+ [% endif %]
24
+ DATABASE_URL=
25
+ POSTGRES_DB=[[ project_slug ]]
26
+ POSTGRES_USER=[[ project_slug ]]
27
+ POSTGRES_PASSWORD=change-me
28
+ POSTGRES_HOST=127.0.0.1
29
+ POSTGRES_PORT=5432
30
+ [% elif use_mysql %]
31
+ [% if use_docker %]
32
+ # Leave blank for local SQLite fallback.
33
+ # DATABASE_URL=sqlite:///db.sqlite3
34
+ # Docker MySQL example:
35
+ # DATABASE_URL=mysql://[[ project_slug ]]:[[ project_slug ]]@db:3306/[[ project_slug ]]
36
+ [% endif %]
37
+ DATABASE_URL=
38
+ MYSQL_DATABASE=[[ project_slug ]]
39
+ MYSQL_USER=[[ project_slug ]]
40
+ MYSQL_PASSWORD=change-me
41
+ MYSQL_HOST=127.0.0.1
42
+ MYSQL_PORT=3306
43
+ [% endif %]
44
+ DB_CONN_MAX_AGE=60
45
+
46
+ [% if use_celery %]
47
+ # ------------------------------------------------------------------------------
48
+ # Redis / Celery
49
+ # ------------------------------------------------------------------------------
50
+ [% if use_docker %]
51
+ REDIS_URL=redis://redis:6379/0
52
+ CELERY_BROKER_URL=redis://redis:6379/0
53
+ CELERY_RESULT_BACKEND=redis://redis:6379/1
54
+ [% else %]
55
+ REDIS_URL=redis://127.0.0.1:6379/0
56
+ CELERY_BROKER_URL=redis://127.0.0.1:6379/0
57
+ CELERY_RESULT_BACKEND=redis://127.0.0.1:6379/1
58
+ [% endif %]
59
+ [% endif %]
60
+
61
+ # ------------------------------------------------------------------------------
62
+ # Deployment / security flags
63
+ # ------------------------------------------------------------------------------
64
+ SECURE_SSL_REDIRECT=False
65
+ RUN_MIGRATIONS=true
66
+ COLLECT_STATIC=false
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .Python
6
+
7
+ # Virtual environments
8
+ .venv/
9
+ venv/
10
+ env/
11
+
12
+ # Django
13
+ db.sqlite3
14
+ *.sqlite3
15
+ media/
16
+ static_root/
17
+
18
+ # Runtime and tooling
19
+ .env
20
+ .env.local
21
+ .pytest_cache/
22
+ .mypy_cache/
23
+ .coverage
24
+ htmlcov/
25
+
26
+ # Build artifacts
27
+ build/
28
+ dist/
29
+
30
+ # IDE / OS files
31
+ .idea/
32
+ .vscode/
33
+ .DS_Store
34
+
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env python
2
+ import sys
3
+
4
+ from core.env import configure_environment
5
+
6
+
7
+ def main() -> None:
8
+ configure_environment()
9
+ from django.core.management import execute_from_command_line
10
+
11
+ execute_from_command_line(sys.argv)
12
+
13
+
14
+ if __name__ == "__main__":
15
+ main()
16
+
@@ -0,0 +1,19 @@
1
+ :root {
2
+ color-scheme: light dark;
3
+ font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ }
9
+
10
+ .container {
11
+ max-width: 960px;
12
+ margin: 0 auto;
13
+ padding: 2rem 1rem;
14
+ }
15
+
16
+ h1 {
17
+ margin-top: 0;
18
+ }
19
+
@@ -0,0 +1,19 @@
1
+ {% load static %}
2
+ <!doctype html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>{% block title %}[[ project_title ]]{% endblock %}</title>
8
+ <link rel="stylesheet" href="{% static 'css/main.css' %}">
9
+ [% if use_htmx %]
10
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
11
+ [% endif %]
12
+ </head>
13
+ <body>
14
+ <main class="container">
15
+ {% block content %}{% endblock %}
16
+ </main>
17
+ </body>
18
+ </html>
19
+
@@ -0,0 +1,7 @@
1
+ from celery import shared_task
2
+
3
+
4
+ @shared_task
5
+ def ping():
6
+ return "pong"
7
+
@@ -0,0 +1,16 @@
1
+ from celery import Celery
2
+
3
+ from core.env import configure_environment
4
+
5
+
6
+ configure_environment()
7
+
8
+ app = Celery("[[ project_slug ]]")
9
+ app.config_from_object("django.conf:settings", namespace="CELERY")
10
+ app.autodiscover_tasks()
11
+
12
+
13
+ @app.task(bind=True)
14
+ def debug_task(self): # pragma: no cover - smoke task
15
+ return f"Celery debug task executed: {self.request!r}"
16
+
@@ -0,0 +1,67 @@
1
+ # Docker Guide
2
+
3
+ This project includes Docker support for local and production-style workflows.
4
+
5
+ ## Services
6
+
7
+ - `web` (Django)
8
+ [% if use_postgres %]- `db` (PostgreSQL)
9
+ [% elif use_mysql %]- `db` (MySQL)
10
+ [% else %]- `db` is not used (SQLite)
11
+ [% endif %]
12
+ [% if use_celery %]- `redis` (broker/result backend)
13
+ - `celery` (worker)
14
+ - `celery-beat` (optional profile)
15
+ [% endif %]
16
+
17
+ ## Start local stack
18
+
19
+ ```bash
20
+ cp .env.example .env
21
+ docker compose build
22
+ docker compose up -d
23
+ docker compose exec web python manage.py check
24
+ ```
25
+
26
+ ## Stop stack
27
+
28
+ ```bash
29
+ docker compose down
30
+ ```
31
+
32
+ ## Production-style compose
33
+
34
+ ```bash
35
+ docker compose -f docker-compose.prod.yml up -d --build
36
+ ```
37
+
38
+ [% if use_celery %]
39
+ ## Celery
40
+
41
+ Inspect worker logs:
42
+
43
+ ```bash
44
+ docker compose logs -f celery
45
+ ```
46
+
47
+ Start beat profile:
48
+
49
+ ```bash
50
+ docker compose --profile beat up -d celery-beat
51
+ ```
52
+ [% endif %]
53
+
54
+ ## Helper script
55
+
56
+ ```bash
57
+ ./docker_deploy.sh up
58
+ ./docker_deploy.sh check
59
+ ./docker_deploy.sh logs web
60
+ ```
61
+
62
+ [% if use_sqlite %]
63
+ ## Note about SQLite with Docker
64
+
65
+ SQLite is supported for development convenience but is not suitable for multi-container production workloads.
66
+ [% endif %]
67
+