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,7 @@
1
+ """Codefortify starter project generator package."""
2
+
3
+ from codefortify_starter.constants import PACKAGE_VERSION
4
+
5
+ __all__ = ["PACKAGE_VERSION", "__version__"]
6
+
7
+ __version__ = PACKAGE_VERSION
@@ -0,0 +1,6 @@
1
+ from codefortify_starter.cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
6
+
@@ -0,0 +1,103 @@
1
+ """CLI entry point for project generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from codefortify_starter.generator import StarterProjectGenerator, build_generation_options
11
+ from codefortify_starter.validators import ValidationError, build_project_identity, normalize_database
12
+
13
+
14
+ console = Console()
15
+
16
+
17
+ def run(
18
+ project_name: str = typer.Argument(..., help="Project directory name to create."),
19
+ htmx: bool = typer.Option(False, "--htmx", help="Include HTMX integration."),
20
+ drf: bool = typer.Option(False, "--drf", help="Include Django REST Framework starter API."),
21
+ docker: bool = typer.Option(False, "--docker", help="Include Docker and Compose files."),
22
+ celery: bool = typer.Option(False, "--celery", help="Include Celery + Redis task setup."),
23
+ database: str | None = typer.Option(
24
+ None,
25
+ "--database",
26
+ help="Database backend to configure: sqlite, postgres, or mysql.",
27
+ ),
28
+ no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization."),
29
+ force: bool = typer.Option(False, "--force", help="Allow generation into an existing directory."),
30
+ directory: Path = typer.Option(Path("."), "--directory", help="Parent directory for the generated project."),
31
+ all_features: bool = typer.Option(
32
+ False,
33
+ "--all",
34
+ help="Enable full-stack preset: --htmx --drf --docker --celery --database postgres.",
35
+ ),
36
+ ) -> None:
37
+ """Generate a Django starter project."""
38
+ try:
39
+ if all_features:
40
+ htmx = True
41
+ drf = True
42
+ docker = True
43
+ celery = True
44
+ if database is None:
45
+ database = "postgres"
46
+
47
+ resolved_database = normalize_database(database, use_docker=docker)
48
+ identity = build_project_identity(project_name)
49
+
50
+ options = build_generation_options(
51
+ project_name=identity.project_name,
52
+ project_slug=identity.project_slug,
53
+ project_title=identity.project_title,
54
+ project_package=identity.project_package,
55
+ project_class_name=identity.project_class_name,
56
+ directory=directory.resolve(),
57
+ database=resolved_database,
58
+ use_htmx=htmx,
59
+ use_drf=drf,
60
+ use_docker=docker,
61
+ use_celery=celery,
62
+ no_git=no_git,
63
+ force=force,
64
+ )
65
+ generator = StarterProjectGenerator()
66
+ result = generator.generate(options)
67
+ except (ValidationError, FileExistsError, NotADirectoryError, RuntimeError) as exc:
68
+ console.print(f"[bold red]Error:[/bold red] {exc}")
69
+ raise typer.Exit(code=1) from exc
70
+
71
+ selected_features = []
72
+ if htmx:
73
+ selected_features.append("htmx")
74
+ if drf:
75
+ selected_features.append("drf")
76
+ if docker:
77
+ selected_features.append("docker")
78
+ if celery:
79
+ selected_features.append("celery")
80
+ features_text = ", ".join(selected_features) if selected_features else "base"
81
+
82
+ console.print(
83
+ f"[bold green]Created[/bold green] {result.project_path} "
84
+ f"(database={resolved_database}, features={features_text})"
85
+ )
86
+ if result.warnings:
87
+ for warning in result.warnings:
88
+ console.print(f"[yellow]Warning:[/yellow] {warning}")
89
+
90
+ console.print("Next steps:")
91
+ console.print(f" 1. cd {result.project_path}")
92
+ console.print(" 2. python -m venv .venv && source .venv/bin/activate")
93
+ console.print(" 3. pip install -r requirements.txt")
94
+ console.print(" 4. cp .env.example .env")
95
+ console.print(" 5. python manage.py migrate && python manage.py runserver")
96
+
97
+
98
+ def main() -> None:
99
+ typer.run(run)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
@@ -0,0 +1,48 @@
1
+ """Shared constants for the starter generator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+
8
+ PACKAGE_NAME: Final[str] = "codefortify-starter"
9
+ IMPORT_PACKAGE: Final[str] = "codefortify_starter"
10
+ CLI_COMMAND: Final[str] = "codefortify-startproject"
11
+ PACKAGE_VERSION: Final[str] = "1.0.0"
12
+
13
+ VALID_DATABASES: Final[tuple[str, ...]] = ("sqlite", "postgres", "mysql")
14
+
15
+ BASE_REQUIREMENTS: Final[tuple[str, ...]] = (
16
+ "Django>=5.2,<5.3",
17
+ "python-decouple>=3.8,<4.0",
18
+ )
19
+
20
+ FEATURE_REQUIREMENTS: Final[dict[str, tuple[str, ...]]] = {
21
+ "htmx": ("django-htmx>=1.20.0,<2.0",),
22
+ "drf": (
23
+ "djangorestframework>=3.15.0,<4.0",
24
+ "django-filter>=24.0,<26.0",
25
+ ),
26
+ "docker": (
27
+ "gunicorn>=23.0.0,<24.0",
28
+ "whitenoise>=6.8.0,<7.0",
29
+ ),
30
+ "celery": (
31
+ "celery>=5.4.0,<6.0",
32
+ "redis>=5.0.0,<6.0",
33
+ ),
34
+ }
35
+
36
+ DATABASE_REQUIREMENTS: Final[dict[str, tuple[str, ...]]] = {
37
+ "sqlite": (),
38
+ "postgres": (
39
+ "dj-database-url>=2.3.0,<3.0",
40
+ "psycopg2-binary>=2.9.9,<3.0",
41
+ ),
42
+ "mysql": (
43
+ "dj-database-url>=2.3.0,<3.0",
44
+ "PyMySQL>=1.1.0,<2.0",
45
+ ),
46
+ }
47
+
48
+ EXECUTABLE_FILES: Final[tuple[str, ...]] = ("entrypoint.sh", "docker_deploy.sh")
@@ -0,0 +1,246 @@
1
+ """Project generation orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ import stat
8
+ import subprocess
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+
12
+ from codefortify_starter.constants import (
13
+ BASE_REQUIREMENTS,
14
+ DATABASE_REQUIREMENTS,
15
+ EXECUTABLE_FILES,
16
+ FEATURE_REQUIREMENTS,
17
+ PACKAGE_VERSION,
18
+ )
19
+ from codefortify_starter.template_engine import TemplateEngine
20
+
21
+
22
+ UNRESOLVED_TOKEN_RE = re.compile(r"(\[\[.+?\]\]|\[%.*?%\])", re.DOTALL)
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class GenerationOptions:
27
+ project_name: str
28
+ project_slug: str
29
+ project_title: str
30
+ project_package: str
31
+ project_class_name: str
32
+ target_root: Path
33
+ database: str
34
+ use_htmx: bool = False
35
+ use_drf: bool = False
36
+ use_docker: bool = False
37
+ use_celery: bool = False
38
+ use_git: bool = True
39
+ force: bool = False
40
+ package_version: str = PACKAGE_VERSION
41
+ created_by: str = "codefortify-starter"
42
+
43
+ @property
44
+ def project_path(self) -> Path:
45
+ return self.target_root / self.project_name
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class GenerationResult:
50
+ project_path: Path
51
+ requirements: list[str]
52
+ warnings: list[str] = field(default_factory=list)
53
+
54
+
55
+ class StarterProjectGenerator:
56
+ """Generates starter projects from base and feature template trees."""
57
+
58
+ def __init__(self, template_root: Path | None = None) -> None:
59
+ self.template_root = template_root or (Path(__file__).resolve().parent / "templates")
60
+ self.engine = TemplateEngine()
61
+
62
+ def generate(self, options: GenerationOptions) -> GenerationResult:
63
+ warnings: list[str] = []
64
+ written_paths: set[Path] = set()
65
+ options.target_root.mkdir(parents=True, exist_ok=True)
66
+ project_path = options.project_path
67
+
68
+ if project_path.exists() and not options.force:
69
+ raise FileExistsError(f"Destination '{project_path}' already exists. Use --force to overwrite safely.")
70
+ if project_path.exists() and not project_path.is_dir():
71
+ raise NotADirectoryError(f"Destination '{project_path}' exists and is not a directory.")
72
+ project_path.mkdir(parents=True, exist_ok=True)
73
+
74
+ context = self._build_context(options)
75
+ written_paths.update(self._render_base(context, project_path))
76
+ written_paths.update(self._render_feature_overlays(context, project_path, options))
77
+
78
+ requirements = self._build_requirements(options)
79
+ written_paths.add(self._write_requirements(project_path, requirements))
80
+ self._assert_no_unresolved_tokens(written_paths)
81
+
82
+ if options.use_docker:
83
+ self._set_executable_bits(project_path)
84
+ if options.database == "sqlite":
85
+ warnings.append("Docker + SQLite is supported for development only and is not recommended for production.")
86
+
87
+ if options.use_git:
88
+ warning = self._initialize_git(project_path)
89
+ if warning:
90
+ warnings.append(warning)
91
+
92
+ return GenerationResult(project_path=project_path, requirements=requirements, warnings=warnings)
93
+
94
+ def _render_base(self, context: dict[str, object], project_path: Path) -> set[Path]:
95
+ base_dir = self.template_root / "base_project"
96
+ written_paths = self.engine.render_tree(base_dir, project_path, context)
97
+ (project_path / "media").mkdir(exist_ok=True)
98
+ media_gitkeep = project_path / "media" / ".gitkeep"
99
+ media_gitkeep.touch(exist_ok=True)
100
+ written_paths.add(media_gitkeep)
101
+ return written_paths
102
+
103
+ def _render_feature_overlays(
104
+ self,
105
+ context: dict[str, object],
106
+ project_path: Path,
107
+ options: GenerationOptions,
108
+ ) -> set[Path]:
109
+ written_paths: set[Path] = set()
110
+ feature_flags = {
111
+ "htmx": options.use_htmx,
112
+ "drf": options.use_drf,
113
+ "docker": options.use_docker,
114
+ "celery": options.use_celery,
115
+ }
116
+ for feature_name, enabled in feature_flags.items():
117
+ if not enabled:
118
+ continue
119
+ feature_dir = self.template_root / "features" / feature_name
120
+ written_paths.update(self.engine.render_tree(feature_dir, project_path, context))
121
+ return written_paths
122
+
123
+ def _build_context(self, options: GenerationOptions) -> dict[str, object]:
124
+ return {
125
+ "project_name": options.project_name,
126
+ "project_slug": options.project_slug,
127
+ "project_title": options.project_title,
128
+ "project_package": options.project_package,
129
+ "project_class_name": options.project_class_name,
130
+ "database": options.database,
131
+ "use_htmx": options.use_htmx,
132
+ "use_drf": options.use_drf,
133
+ "use_docker": options.use_docker,
134
+ "use_celery": options.use_celery,
135
+ "use_postgres": options.database == "postgres",
136
+ "use_mysql": options.database == "mysql",
137
+ "use_sqlite": options.database == "sqlite",
138
+ "use_git": options.use_git,
139
+ "package_version": options.package_version,
140
+ "created_by": options.created_by,
141
+ }
142
+
143
+ def _build_requirements(self, options: GenerationOptions) -> list[str]:
144
+ dependencies: list[str] = list(BASE_REQUIREMENTS)
145
+
146
+ if options.use_htmx:
147
+ dependencies.extend(FEATURE_REQUIREMENTS["htmx"])
148
+ if options.use_drf:
149
+ dependencies.extend(FEATURE_REQUIREMENTS["drf"])
150
+ if options.use_docker:
151
+ dependencies.extend(FEATURE_REQUIREMENTS["docker"])
152
+ if options.use_celery:
153
+ dependencies.extend(FEATURE_REQUIREMENTS["celery"])
154
+
155
+ dependencies.extend(DATABASE_REQUIREMENTS[options.database])
156
+
157
+ # Stable dedupe order
158
+ deduped = list(dict.fromkeys(dependencies))
159
+ return deduped
160
+
161
+ def _write_requirements(self, project_path: Path, dependencies: list[str]) -> Path:
162
+ requirements_path = project_path / "requirements.txt"
163
+ body = "\n".join(dependencies) + "\n"
164
+ requirements_path.write_text(body, encoding="utf-8")
165
+ return requirements_path
166
+
167
+ def _set_executable_bits(self, project_path: Path) -> None:
168
+ for relative_path in EXECUTABLE_FILES:
169
+ file_path = project_path / relative_path
170
+ if not file_path.exists():
171
+ continue
172
+ mode = file_path.stat().st_mode
173
+ file_path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
174
+
175
+ def _assert_no_unresolved_tokens(self, paths: set[Path]) -> None:
176
+ for path in sorted(paths):
177
+ if not path.exists() or not path.is_file():
178
+ continue
179
+ try:
180
+ content = path.read_text(encoding="utf-8")
181
+ except UnicodeDecodeError:
182
+ continue
183
+ if UNRESOLVED_TOKEN_RE.search(content):
184
+ raise RuntimeError(f"Unresolved template token found in generated file: {path}")
185
+
186
+ def _initialize_git(self, project_path: Path) -> str | None:
187
+ try:
188
+ completed = subprocess.run(
189
+ ["git", "init"],
190
+ cwd=project_path,
191
+ check=False,
192
+ stdout=subprocess.DEVNULL,
193
+ stderr=subprocess.PIPE,
194
+ text=True,
195
+ )
196
+ except FileNotFoundError:
197
+ return "Git is not installed; skipped repository initialization."
198
+
199
+ if completed.returncode != 0:
200
+ message = (completed.stderr or "").strip() or "Unknown git init failure."
201
+ return f"Failed to initialize git repository: {message}"
202
+
203
+ # Avoid hard failure if user shell/environment does not support this command.
204
+ subprocess.run(
205
+ ["git", "symbolic-ref", "HEAD", "refs/heads/main"],
206
+ cwd=project_path,
207
+ check=False,
208
+ stdout=subprocess.DEVNULL,
209
+ stderr=subprocess.DEVNULL,
210
+ )
211
+ return None
212
+
213
+
214
+ def build_generation_options(
215
+ *,
216
+ project_name: str,
217
+ project_slug: str,
218
+ project_title: str,
219
+ project_package: str,
220
+ project_class_name: str,
221
+ directory: Path,
222
+ database: str,
223
+ use_htmx: bool,
224
+ use_drf: bool,
225
+ use_docker: bool,
226
+ use_celery: bool,
227
+ no_git: bool,
228
+ force: bool,
229
+ ) -> GenerationOptions:
230
+ created_by = os.environ.get("USER") or os.environ.get("USERNAME") or "codefortify"
231
+ return GenerationOptions(
232
+ project_name=project_name,
233
+ project_slug=project_slug,
234
+ project_title=project_title,
235
+ project_package=project_package,
236
+ project_class_name=project_class_name,
237
+ target_root=directory,
238
+ database=database,
239
+ use_htmx=use_htmx,
240
+ use_drf=use_drf,
241
+ use_docker=use_docker,
242
+ use_celery=use_celery,
243
+ use_git=not no_git,
244
+ force=force,
245
+ created_by=created_by,
246
+ )
@@ -0,0 +1,63 @@
1
+ """Filesystem + Jinja template rendering utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from jinja2 import Environment, StrictUndefined
9
+
10
+
11
+ class TemplateEngine:
12
+ """Renders template trees using custom Jinja delimiters."""
13
+
14
+ def __init__(self) -> None:
15
+ self._environment = Environment(
16
+ autoescape=False,
17
+ keep_trailing_newline=True,
18
+ trim_blocks=False,
19
+ lstrip_blocks=False,
20
+ undefined=StrictUndefined,
21
+ variable_start_string="[[",
22
+ variable_end_string="]]",
23
+ block_start_string="[%", # avoids collision with Django template tags
24
+ block_end_string="%]",
25
+ )
26
+
27
+ def render_tree(self, source_root: Path, destination_root: Path, context: dict[str, object]) -> set[Path]:
28
+ """Render files from `source_root` into `destination_root`."""
29
+ written_paths: set[Path] = set()
30
+ if not source_root.exists():
31
+ return written_paths
32
+
33
+ for source_path in sorted(source_root.rglob("*")):
34
+ relative = source_path.relative_to(source_root)
35
+ destination_path = destination_root / self._normalized_relative_path(relative)
36
+ if source_path.is_dir():
37
+ destination_path.mkdir(parents=True, exist_ok=True)
38
+ continue
39
+
40
+ destination_path.parent.mkdir(parents=True, exist_ok=True)
41
+ if source_path.suffix == ".jinja":
42
+ rendered = self.render_text(source_path.read_text(encoding="utf-8"), context)
43
+ destination_path.write_text(rendered, encoding="utf-8")
44
+ else:
45
+ shutil.copy2(source_path, destination_path)
46
+ written_paths.add(destination_path)
47
+
48
+ return written_paths
49
+
50
+ def render_text(self, template_source: str, context: dict[str, object]) -> str:
51
+ template = self._environment.from_string(template_source)
52
+ return template.render(**context)
53
+
54
+ def _normalized_relative_path(self, relative_path: Path) -> Path:
55
+ parts = []
56
+ for part in relative_path.parts:
57
+ normalized = part
58
+ if normalized.startswith("dot-"):
59
+ normalized = f".{normalized[4:]}"
60
+ if normalized.endswith(".jinja"):
61
+ normalized = normalized[: -len(".jinja")]
62
+ parts.append(normalized)
63
+ return Path(*parts)
@@ -0,0 +1,87 @@
1
+ # [[ project_title ]]
2
+
3
+ Generated with `codefortify-starter` ([[ package_version ]]).
4
+
5
+ ## Selected features
6
+
7
+ - Database: `[[ database ]]`
8
+ - HTMX: `[% if use_htmx %]enabled[% else %]disabled[% endif %]`
9
+ - DRF: `[% if use_drf %]enabled[% else %]disabled[% endif %]`
10
+ - Docker: `[% if use_docker %]enabled[% else %]disabled[% endif %]`
11
+ - Celery: `[% if use_celery %]enabled[% else %]disabled[% endif %]`
12
+
13
+ ## Quick start
14
+
15
+ ```bash
16
+ python -m venv .venv
17
+ source .venv/bin/activate
18
+ pip install -r requirements.txt
19
+ cp .env.example .env
20
+ python manage.py migrate
21
+ python manage.py runserver
22
+ ```
23
+
24
+ Run checks:
25
+
26
+ ```bash
27
+ python manage.py check
28
+ python manage.py test
29
+ ```
30
+
31
+ [% if use_drf %]
32
+ ## API
33
+
34
+ Base API route:
35
+
36
+ - `GET /api/health/`
37
+
38
+ [% endif %]
39
+ [% if use_htmx %]
40
+ ## HTMX
41
+
42
+ HTMX demo endpoint:
43
+
44
+ - `GET /htmx/example/`
45
+
46
+ [% endif %]
47
+ [% if use_celery %]
48
+ ## Celery
49
+
50
+ Run worker locally:
51
+
52
+ ```bash
53
+ celery -A core worker -l info
54
+ ```
55
+
56
+ Redis must be available at `REDIS_URL`.
57
+
58
+ [% endif %]
59
+ [% if use_docker %]
60
+ ## Docker
61
+
62
+ Development compose:
63
+
64
+ ```bash
65
+ docker compose up -d --build
66
+ docker compose exec web python manage.py check
67
+ ```
68
+
69
+ See `DOCKER_README.md` for deployment details.
70
+
71
+ [% endif %]
72
+ ## Project layout
73
+
74
+ ```
75
+ .
76
+ |-- apps/
77
+ | |-- home/
78
+ [% if use_drf %]| `-- api/
79
+ [% endif %]|-- core/
80
+ | `-- settings/
81
+ |-- templates/
82
+ |-- static/
83
+ |-- media/
84
+ |-- requirements.txt
85
+ `-- manage.py
86
+ ```
87
+
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class HomeConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "apps.home"
7
+
@@ -0,0 +1,25 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Home | [[ project_title ]]{% endblock %}
4
+
5
+ {% block content %}
6
+ <h1>[[ project_title ]]</h1>
7
+ <p>Generated with codefortify-starter.</p>
8
+
9
+ [% if use_htmx %]
10
+ <section>
11
+ <button
12
+ hx-get="{% url 'home:htmx-example' %}"
13
+ hx-target="#htmx-fragment"
14
+ hx-swap="innerHTML"
15
+ type="button"
16
+ >
17
+ Load HTMX Fragment
18
+ </button>
19
+ <div id="htmx-fragment">
20
+ {% include "partials/example.html" %}
21
+ </div>
22
+ </section>
23
+ [% endif %]
24
+ {% endblock %}
25
+
@@ -0,0 +1,17 @@
1
+ from django.test import TestCase
2
+ from django.urls import reverse
3
+
4
+
5
+ class HomeViewTests(TestCase):
6
+ def test_home_page_renders(self):
7
+ response = self.client.get(reverse("home:home"))
8
+ self.assertEqual(response.status_code, 200)
9
+ self.assertTemplateUsed(response, "home/index.html")
10
+
11
+ [% if use_htmx %]
12
+ def test_htmx_example_partial_renders(self):
13
+ response = self.client.get(reverse("home:htmx-example"), HTTP_HX_REQUEST="true")
14
+ self.assertEqual(response.status_code, 200)
15
+ self.assertTemplateUsed(response, "partials/example.html")
16
+ [% endif %]
17
+
@@ -0,0 +1,14 @@
1
+ from django.urls import path
2
+
3
+ from apps.home.views import HomeView[% if use_htmx %], htmx_example[% endif %]
4
+
5
+
6
+ app_name = "home"
7
+
8
+ urlpatterns = [
9
+ path("", HomeView.as_view(), name="home"),
10
+ [% if use_htmx %]
11
+ path("htmx/example/", htmx_example, name="htmx-example"),
12
+ [% endif %]
13
+ ]
14
+
@@ -0,0 +1,13 @@
1
+ from django.shortcuts import render
2
+ from django.views.generic import TemplateView
3
+
4
+
5
+ class HomeView(TemplateView):
6
+ template_name = "home/index.html"
7
+
8
+
9
+ [% if use_htmx %]
10
+ def htmx_example(request):
11
+ return render(request, "partials/example.html")
12
+ [% endif %]
13
+
@@ -0,0 +1,7 @@
1
+ [% if use_celery %]
2
+ from core.celery import app as celery_app
3
+ [% endif %]
4
+
5
+ __version__ = "1.0.0"
6
+
7
+ __all__ = ["__version__"[% if use_celery %], "celery_app"[% endif %]]
@@ -0,0 +1,10 @@
1
+ from core.env import configure_environment
2
+
3
+
4
+ configure_environment()
5
+
6
+ from django.core.asgi import get_asgi_application
7
+
8
+
9
+ application = get_asgi_application()
10
+