fastapi-forge-cli 0.1.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 (181) hide show
  1. fastapi_forge/__init__.py +7 -0
  2. fastapi_forge/__main__.py +6 -0
  3. fastapi_forge/cli.py +211 -0
  4. fastapi_forge/templates/with_rbac/Dockerfile +31 -0
  5. fastapi_forge/templates/with_rbac/README.md +121 -0
  6. fastapi_forge/templates/with_rbac/_dockerignore +16 -0
  7. fastapi_forge/templates/with_rbac/_github/workflows/ci.yml +23 -0
  8. fastapi_forge/templates/with_rbac/_gitignore +19 -0
  9. fastapi_forge/templates/with_rbac/alembic/README +1 -0
  10. fastapi_forge/templates/with_rbac/alembic/__init__.py +1 -0
  11. fastapi_forge/templates/with_rbac/alembic/env.py +51 -0
  12. fastapi_forge/templates/with_rbac/alembic/script.py.mako +28 -0
  13. fastapi_forge/templates/with_rbac/alembic/versions/2255ba4f9604_fresh_baseline.py +204 -0
  14. fastapi_forge/templates/with_rbac/alembic.ini +35 -0
  15. fastapi_forge/templates/with_rbac/app/__init__.py +1 -0
  16. fastapi_forge/templates/with_rbac/app/api/__init__.py +1 -0
  17. fastapi_forge/templates/with_rbac/app/api/v1/__init__.py +1 -0
  18. fastapi_forge/templates/with_rbac/app/api/v1/api.py +34 -0
  19. fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/__init__.py +1 -0
  20. fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/repository.py +38 -0
  21. fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/router.py +50 -0
  22. fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/schema.py +21 -0
  23. fastapi_forge/templates/with_rbac/app/api/v1/auth/__init__.py +0 -0
  24. fastapi_forge/templates/with_rbac/app/api/v1/auth/repository.py +179 -0
  25. fastapi_forge/templates/with_rbac/app/api/v1/auth/router.py +209 -0
  26. fastapi_forge/templates/with_rbac/app/api/v1/auth/schema.py +98 -0
  27. fastapi_forge/templates/with_rbac/app/api/v1/auth/service.py +383 -0
  28. fastapi_forge/templates/with_rbac/app/api/v1/health/__init__.py +3 -0
  29. fastapi_forge/templates/with_rbac/app/api/v1/health/router.py +23 -0
  30. fastapi_forge/templates/with_rbac/app/api/v1/health/schema.py +5 -0
  31. fastapi_forge/templates/with_rbac/app/api/v1/health/service.py +25 -0
  32. fastapi_forge/templates/with_rbac/app/api/v1/permissions/__init__.py +0 -0
  33. fastapi_forge/templates/with_rbac/app/api/v1/permissions/repository.py +80 -0
  34. fastapi_forge/templates/with_rbac/app/api/v1/permissions/router.py +151 -0
  35. fastapi_forge/templates/with_rbac/app/api/v1/permissions/schema.py +40 -0
  36. fastapi_forge/templates/with_rbac/app/api/v1/permissions/service.py +156 -0
  37. fastapi_forge/templates/with_rbac/app/api/v1/roles/__init__.py +1 -0
  38. fastapi_forge/templates/with_rbac/app/api/v1/roles/repository.py +161 -0
  39. fastapi_forge/templates/with_rbac/app/api/v1/roles/router.py +169 -0
  40. fastapi_forge/templates/with_rbac/app/api/v1/roles/schema.py +51 -0
  41. fastapi_forge/templates/with_rbac/app/api/v1/roles/service.py +319 -0
  42. fastapi_forge/templates/with_rbac/app/api/v1/schema.py +7 -0
  43. fastapi_forge/templates/with_rbac/app/api/v1/users/__init__.py +1 -0
  44. fastapi_forge/templates/with_rbac/app/api/v1/users/repository.py +181 -0
  45. fastapi_forge/templates/with_rbac/app/api/v1/users/router.py +146 -0
  46. fastapi_forge/templates/with_rbac/app/api/v1/users/schema.py +112 -0
  47. fastapi_forge/templates/with_rbac/app/api/v1/users/service.py +291 -0
  48. fastapi_forge/templates/with_rbac/app/core/__init__.py +1 -0
  49. fastapi_forge/templates/with_rbac/app/core/config.py +131 -0
  50. fastapi_forge/templates/with_rbac/app/core/dependencies.py +131 -0
  51. fastapi_forge/templates/with_rbac/app/core/exceptions.py +162 -0
  52. fastapi_forge/templates/with_rbac/app/core/logging.py +231 -0
  53. fastapi_forge/templates/with_rbac/app/core/middleware.py +188 -0
  54. fastapi_forge/templates/with_rbac/app/core/responses.py +108 -0
  55. fastapi_forge/templates/with_rbac/app/core/security.py +115 -0
  56. fastapi_forge/templates/with_rbac/app/db/__init__.py +1 -0
  57. fastapi_forge/templates/with_rbac/app/db/base.py +5 -0
  58. fastapi_forge/templates/with_rbac/app/db/models/__init__.py +16 -0
  59. fastapi_forge/templates/with_rbac/app/db/models/audit_log.py +58 -0
  60. fastapi_forge/templates/with_rbac/app/db/models/auth_token.py +72 -0
  61. fastapi_forge/templates/with_rbac/app/db/models/notification.py +49 -0
  62. fastapi_forge/templates/with_rbac/app/db/models/permission.py +174 -0
  63. fastapi_forge/templates/with_rbac/app/db/models/revoked_token.py +21 -0
  64. fastapi_forge/templates/with_rbac/app/db/models/user.py +53 -0
  65. fastapi_forge/templates/with_rbac/app/db/schemas/__init__.py +8 -0
  66. fastapi_forge/templates/with_rbac/app/db/schemas/common.py +70 -0
  67. fastapi_forge/templates/with_rbac/app/db/schemas/names.py +9 -0
  68. fastapi_forge/templates/with_rbac/app/db/session.py +86 -0
  69. fastapi_forge/templates/with_rbac/app/helper/__init__.py +1 -0
  70. fastapi_forge/templates/with_rbac/app/helper/pagination_helper.py +44 -0
  71. fastapi_forge/templates/with_rbac/app/helper/search.py +51 -0
  72. fastapi_forge/templates/with_rbac/app/helper/sorting.py +77 -0
  73. fastapi_forge/templates/with_rbac/app/main.py +66 -0
  74. fastapi_forge/templates/with_rbac/app/repositories/__init__.py +1 -0
  75. fastapi_forge/templates/with_rbac/app/repositories/base.py +347 -0
  76. fastapi_forge/templates/with_rbac/app/services/__init__.py +1 -0
  77. fastapi_forge/templates/with_rbac/app/services/audit.py +58 -0
  78. fastapi_forge/templates/with_rbac/app/services/email.py +118 -0
  79. fastapi_forge/templates/with_rbac/app/services/notification.py +82 -0
  80. fastapi_forge/templates/with_rbac/app/templates/email/notification.html +7 -0
  81. fastapi_forge/templates/with_rbac/app/templates/email/password_reset.html +7 -0
  82. fastapi_forge/templates/with_rbac/app/templates/email/verify_email.html +7 -0
  83. fastapi_forge/templates/with_rbac/app/templates/email/welcome.html +6 -0
  84. fastapi_forge/templates/with_rbac/app/utils/casing.py +31 -0
  85. fastapi_forge/templates/with_rbac/compose.yaml +33 -0
  86. fastapi_forge/templates/with_rbac/pyproject.toml +14 -0
  87. fastapi_forge/templates/with_rbac/requirements-dev.txt +5 -0
  88. fastapi_forge/templates/with_rbac/requirements.txt +16 -0
  89. fastapi_forge/templates/with_rbac/sample.env +42 -0
  90. fastapi_forge/templates/with_rbac/scripts/seed_first_user.py +166 -0
  91. fastapi_forge/templates/with_rbac/tests/test_audit.py +42 -0
  92. fastapi_forge/templates/with_rbac/tests/test_config.py +27 -0
  93. fastapi_forge/templates/with_rbac/tests/test_generator.py +20 -0
  94. fastapi_forge/templates/with_rbac/tests/test_permissions.py +36 -0
  95. fastapi_forge/templates/with_rbac/tests/test_security.py +68 -0
  96. fastapi_forge/templates/without_rbac/Dockerfile +31 -0
  97. fastapi_forge/templates/without_rbac/README.md +106 -0
  98. fastapi_forge/templates/without_rbac/_dockerignore +16 -0
  99. fastapi_forge/templates/without_rbac/_github/workflows/ci.yml +23 -0
  100. fastapi_forge/templates/without_rbac/_gitignore +19 -0
  101. fastapi_forge/templates/without_rbac/alembic/README +1 -0
  102. fastapi_forge/templates/without_rbac/alembic/__init__.py +1 -0
  103. fastapi_forge/templates/without_rbac/alembic/env.py +51 -0
  104. fastapi_forge/templates/without_rbac/alembic/script.py.mako +28 -0
  105. fastapi_forge/templates/without_rbac/alembic/versions/2255ba4f9604_fresh_baseline.py +125 -0
  106. fastapi_forge/templates/without_rbac/alembic.ini +35 -0
  107. fastapi_forge/templates/without_rbac/app/__init__.py +1 -0
  108. fastapi_forge/templates/without_rbac/app/api/__init__.py +1 -0
  109. fastapi_forge/templates/without_rbac/app/api/v1/__init__.py +1 -0
  110. fastapi_forge/templates/without_rbac/app/api/v1/api.py +29 -0
  111. fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/__init__.py +1 -0
  112. fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/repository.py +38 -0
  113. fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/router.py +45 -0
  114. fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/schema.py +21 -0
  115. fastapi_forge/templates/without_rbac/app/api/v1/auth/__init__.py +0 -0
  116. fastapi_forge/templates/without_rbac/app/api/v1/auth/repository.py +127 -0
  117. fastapi_forge/templates/without_rbac/app/api/v1/auth/router.py +207 -0
  118. fastapi_forge/templates/without_rbac/app/api/v1/auth/schema.py +96 -0
  119. fastapi_forge/templates/without_rbac/app/api/v1/auth/service.py +373 -0
  120. fastapi_forge/templates/without_rbac/app/api/v1/health/__init__.py +3 -0
  121. fastapi_forge/templates/without_rbac/app/api/v1/health/router.py +23 -0
  122. fastapi_forge/templates/without_rbac/app/api/v1/health/schema.py +5 -0
  123. fastapi_forge/templates/without_rbac/app/api/v1/health/service.py +25 -0
  124. fastapi_forge/templates/without_rbac/app/api/v1/schema.py +7 -0
  125. fastapi_forge/templates/without_rbac/app/api/v1/users/__init__.py +1 -0
  126. fastapi_forge/templates/without_rbac/app/api/v1/users/repository.py +69 -0
  127. fastapi_forge/templates/without_rbac/app/api/v1/users/router.py +94 -0
  128. fastapi_forge/templates/without_rbac/app/api/v1/users/schema.py +50 -0
  129. fastapi_forge/templates/without_rbac/app/api/v1/users/service.py +92 -0
  130. fastapi_forge/templates/without_rbac/app/core/__init__.py +1 -0
  131. fastapi_forge/templates/without_rbac/app/core/config.py +131 -0
  132. fastapi_forge/templates/without_rbac/app/core/dependencies.py +71 -0
  133. fastapi_forge/templates/without_rbac/app/core/exceptions.py +162 -0
  134. fastapi_forge/templates/without_rbac/app/core/logging.py +231 -0
  135. fastapi_forge/templates/without_rbac/app/core/middleware.py +188 -0
  136. fastapi_forge/templates/without_rbac/app/core/responses.py +108 -0
  137. fastapi_forge/templates/without_rbac/app/core/security.py +115 -0
  138. fastapi_forge/templates/without_rbac/app/db/__init__.py +1 -0
  139. fastapi_forge/templates/without_rbac/app/db/base.py +5 -0
  140. fastapi_forge/templates/without_rbac/app/db/models/__init__.py +10 -0
  141. fastapi_forge/templates/without_rbac/app/db/models/audit_log.py +58 -0
  142. fastapi_forge/templates/without_rbac/app/db/models/auth_token.py +72 -0
  143. fastapi_forge/templates/without_rbac/app/db/models/notification.py +49 -0
  144. fastapi_forge/templates/without_rbac/app/db/models/revoked_token.py +21 -0
  145. fastapi_forge/templates/without_rbac/app/db/models/user.py +33 -0
  146. fastapi_forge/templates/without_rbac/app/db/schemas/__init__.py +8 -0
  147. fastapi_forge/templates/without_rbac/app/db/schemas/common.py +70 -0
  148. fastapi_forge/templates/without_rbac/app/db/schemas/names.py +5 -0
  149. fastapi_forge/templates/without_rbac/app/db/session.py +86 -0
  150. fastapi_forge/templates/without_rbac/app/helper/__init__.py +1 -0
  151. fastapi_forge/templates/without_rbac/app/helper/pagination_helper.py +44 -0
  152. fastapi_forge/templates/without_rbac/app/helper/search.py +51 -0
  153. fastapi_forge/templates/without_rbac/app/helper/sorting.py +77 -0
  154. fastapi_forge/templates/without_rbac/app/main.py +66 -0
  155. fastapi_forge/templates/without_rbac/app/repositories/__init__.py +1 -0
  156. fastapi_forge/templates/without_rbac/app/repositories/base.py +347 -0
  157. fastapi_forge/templates/without_rbac/app/services/__init__.py +1 -0
  158. fastapi_forge/templates/without_rbac/app/services/audit.py +58 -0
  159. fastapi_forge/templates/without_rbac/app/services/email.py +118 -0
  160. fastapi_forge/templates/without_rbac/app/services/notification.py +82 -0
  161. fastapi_forge/templates/without_rbac/app/templates/email/notification.html +7 -0
  162. fastapi_forge/templates/without_rbac/app/templates/email/password_reset.html +7 -0
  163. fastapi_forge/templates/without_rbac/app/templates/email/verify_email.html +7 -0
  164. fastapi_forge/templates/without_rbac/app/templates/email/welcome.html +6 -0
  165. fastapi_forge/templates/without_rbac/app/utils/casing.py +31 -0
  166. fastapi_forge/templates/without_rbac/compose.yaml +33 -0
  167. fastapi_forge/templates/without_rbac/pyproject.toml +14 -0
  168. fastapi_forge/templates/without_rbac/requirements-dev.txt +5 -0
  169. fastapi_forge/templates/without_rbac/requirements.txt +16 -0
  170. fastapi_forge/templates/without_rbac/sample.env +42 -0
  171. fastapi_forge/templates/without_rbac/scripts/seed_first_user.py +51 -0
  172. fastapi_forge/templates/without_rbac/tests/test_audit.py +42 -0
  173. fastapi_forge/templates/without_rbac/tests/test_config.py +27 -0
  174. fastapi_forge/templates/without_rbac/tests/test_generator.py +20 -0
  175. fastapi_forge/templates/without_rbac/tests/test_security.py +68 -0
  176. fastapi_forge_cli-0.1.0.dist-info/METADATA +225 -0
  177. fastapi_forge_cli-0.1.0.dist-info/RECORD +181 -0
  178. fastapi_forge_cli-0.1.0.dist-info/WHEEL +5 -0
  179. fastapi_forge_cli-0.1.0.dist-info/entry_points.txt +2 -0
  180. fastapi_forge_cli-0.1.0.dist-info/licenses/LICENSE +18 -0
  181. fastapi_forge_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,7 @@
1
+ """FastAPI Forge project generator."""
2
+
3
+ from fastapi_forge.cli import ForgeError, create_project, project_slug
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ __all__ = ["ForgeError", "__version__", "create_project", "project_slug"]
@@ -0,0 +1,6 @@
1
+ """Module entry point for ``python -m fastapi_forge``."""
2
+
3
+ from fastapi_forge.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())
fastapi_forge/cli.py ADDED
@@ -0,0 +1,211 @@
1
+ """Command-line interface for FastAPI Forge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import re
8
+ import shutil
9
+ import sys
10
+ import tempfile
11
+ import uuid
12
+ from collections.abc import Iterable
13
+ from importlib import resources
14
+ from pathlib import Path
15
+
16
+ PACKAGE = "fastapi_forge"
17
+ TEMPLATE_SENTINEL = "__PROJECT_NAME__"
18
+ DATABASE_SENTINEL = "project_name"
19
+ IGNORED_TEMPLATE_NAMES = {
20
+ ".mypy_cache",
21
+ ".pytest_cache",
22
+ ".ruff_cache",
23
+ "__pycache__",
24
+ }
25
+ EXAMPLES = """Examples:
26
+ fastapi-forge create "Inventory API" --with-rbac
27
+ fastapi-forge create "Public API" --without-rbac
28
+ fastapi-forge create "Billing API" --with-rbac --output-dir ./projects
29
+ fastapi-forge create "Inventory API" --with-rbac --force
30
+
31
+ Run 'fastapi-forge create --help' for all project-generation options.
32
+ """
33
+
34
+
35
+ class ForgeError(ValueError):
36
+ pass
37
+
38
+
39
+ def project_slug(value: str) -> str:
40
+ slug = re.sub(r"[^a-zA-Z0-9_-]+", "-", value.strip()).strip("-_").lower()
41
+ if not slug:
42
+ raise ForgeError("Project name must contain a letter or number.")
43
+ return slug
44
+
45
+
46
+ def ask_for_rbac(default: bool = True) -> bool:
47
+ suffix = "Y/n" if default else "y/N"
48
+ while True:
49
+ answer = input(f"Include role-based access control? [{suffix}] ").strip().lower()
50
+ if not answer:
51
+ return default
52
+ if answer in {"y", "yes"}:
53
+ return True
54
+ if answer in {"n", "no"}:
55
+ return False
56
+ print("Please answer yes or no.")
57
+
58
+
59
+ def resolve_rbac(args: argparse.Namespace) -> bool:
60
+ if args.with_rbac:
61
+ return True
62
+ if args.without_rbac:
63
+ return False
64
+ return ask_for_rbac() if sys.stdin.isatty() else True
65
+
66
+
67
+ def copy_resource_tree(source, destination: Path) -> None:
68
+ destination.mkdir(parents=True, exist_ok=True)
69
+ for item in source.iterdir():
70
+ if item.name in IGNORED_TEMPLATE_NAMES or item.name.endswith((".pyc", ".pyo")):
71
+ continue
72
+ target_name = {
73
+ "_gitignore": ".gitignore",
74
+ "_dockerignore": ".dockerignore",
75
+ "_github": ".github",
76
+ }.get(item.name, item.name)
77
+ target = destination / target_name
78
+ if item.is_dir():
79
+ copy_resource_tree(item, target)
80
+ else:
81
+ target.write_bytes(item.read_bytes())
82
+
83
+
84
+ def personalize(destination: Path, display_name: str, slug: str) -> None:
85
+ replacements = {
86
+ TEMPLATE_SENTINEL: display_name,
87
+ DATABASE_SENTINEL: slug.replace("-", "_"),
88
+ }
89
+ for relative in ("README.md", "sample.env", "app/core/config.py"):
90
+ path = destination / relative
91
+ text = path.read_text(encoding="utf-8")
92
+ for old, new in replacements.items():
93
+ text = text.replace(old, new)
94
+ path.write_text(text, encoding="utf-8")
95
+
96
+
97
+ def create_project(
98
+ name: str,
99
+ output_dir: Path,
100
+ with_rbac: bool,
101
+ force: bool = False,
102
+ ) -> Path:
103
+ slug = project_slug(name)
104
+ parent = output_dir.expanduser().resolve()
105
+ destination = parent / slug
106
+
107
+ if destination.exists():
108
+ if not force:
109
+ raise ForgeError(
110
+ f"Destination already exists: {destination}. Use --force to replace it."
111
+ )
112
+ if destination == Path.cwd().resolve():
113
+ raise ForgeError("Refusing to replace the current working directory.")
114
+ if destination.is_symlink():
115
+ raise ForgeError("Refusing to replace a symbolic-link destination.")
116
+
117
+ variant = "with_rbac" if with_rbac else "without_rbac"
118
+ template = resources.files(PACKAGE).joinpath("templates", variant)
119
+ if not template.is_dir():
120
+ raise ForgeError(f"Packaged template is missing: {variant}")
121
+
122
+ parent.mkdir(parents=True, exist_ok=True)
123
+ staging = Path(tempfile.mkdtemp(prefix=f".{slug}-", dir=parent))
124
+ backup: Path | None = None
125
+ try:
126
+ copy_resource_tree(template, staging)
127
+ personalize(staging, name.strip(), slug)
128
+ if destination.exists():
129
+ backup = parent / f".{slug}.backup-{uuid.uuid4().hex}"
130
+ os.replace(destination, backup)
131
+ os.replace(staging, destination)
132
+ if backup is not None:
133
+ shutil.rmtree(backup)
134
+ except Exception:
135
+ shutil.rmtree(staging, ignore_errors=True)
136
+ if backup is not None and backup.exists() and not destination.exists():
137
+ os.replace(backup, destination)
138
+ raise
139
+ return destination
140
+
141
+
142
+ def build_parser() -> argparse.ArgumentParser:
143
+ parser = argparse.ArgumentParser(
144
+ prog="fastapi-forge",
145
+ description="Generate ready-to-run, production-oriented FastAPI projects.",
146
+ epilog="Run 'fastapi-forge examples' to see common commands.",
147
+ )
148
+ parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
149
+ commands = parser.add_subparsers(dest="command", required=True)
150
+
151
+ create = commands.add_parser(
152
+ "create",
153
+ help="Create a new FastAPI project.",
154
+ description="Generate a FastAPI project with PostgreSQL and optional RBAC.",
155
+ epilog=EXAMPLES,
156
+ formatter_class=argparse.RawDescriptionHelpFormatter,
157
+ )
158
+ create.add_argument(
159
+ "name",
160
+ help='Project display name; for example, "Inventory API".',
161
+ )
162
+ create.add_argument(
163
+ "--output-dir",
164
+ type=Path,
165
+ default=Path.cwd(),
166
+ help="Parent directory for the generated project (default: current directory).",
167
+ )
168
+ choice = create.add_mutually_exclusive_group()
169
+ choice.add_argument(
170
+ "--with-rbac",
171
+ action="store_true",
172
+ help="Include roles, permissions, and RBAC administration APIs.",
173
+ )
174
+ choice.add_argument(
175
+ "--without-rbac",
176
+ action="store_true",
177
+ help="Use simpler superuser-based administration without RBAC.",
178
+ )
179
+ create.add_argument(
180
+ "--force",
181
+ action="store_true",
182
+ help="Replace the matching destination after generating a safe backup.",
183
+ )
184
+ commands.add_parser(
185
+ "examples",
186
+ help="Show common FastAPI Forge commands.",
187
+ description="Show copy-ready examples for generating projects.",
188
+ )
189
+ return parser
190
+
191
+
192
+ def main(argv: Iterable[str] | None = None) -> int:
193
+ parser = build_parser()
194
+ args = parser.parse_args(argv)
195
+
196
+ if args.command == "examples":
197
+ print(EXAMPLES)
198
+ return 0
199
+
200
+ try:
201
+ destination = create_project(
202
+ name=args.name,
203
+ output_dir=args.output_dir,
204
+ with_rbac=resolve_rbac(args),
205
+ force=args.force,
206
+ )
207
+ except ForgeError as exc:
208
+ parser.error(str(exc))
209
+
210
+ print(f"Created project: {destination}")
211
+ return 0
@@ -0,0 +1,31 @@
1
+ FROM python:3.12-slim AS builder
2
+
3
+ ENV PIP_DISABLE_PIP_VERSION_CHECK=1 \
4
+ PIP_NO_CACHE_DIR=1 \
5
+ PYTHONDONTWRITEBYTECODE=1
6
+
7
+ WORKDIR /build
8
+ COPY requirements.txt .
9
+ RUN python -m venv /opt/venv && /opt/venv/bin/pip install --upgrade pip && \
10
+ /opt/venv/bin/pip install -r requirements.txt
11
+
12
+ FROM python:3.12-slim AS runtime
13
+
14
+ ENV PATH="/opt/venv/bin:$PATH" \
15
+ PYTHONUNBUFFERED=1 \
16
+ PYTHONDONTWRITEBYTECODE=1
17
+
18
+ RUN groupadd --system app && useradd --system --gid app --create-home app
19
+ WORKDIR /app
20
+ COPY --from=builder /opt/venv /opt/venv
21
+ COPY --chown=app:app alembic alembic
22
+ COPY --chown=app:app alembic.ini .
23
+ COPY --chown=app:app app app
24
+ COPY --chown=app:app scripts scripts
25
+
26
+ USER app
27
+ EXPOSE 8000
28
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
29
+ CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/v1/health/live', timeout=3)"
30
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2", "--proxy-headers"]
31
+
@@ -0,0 +1,121 @@
1
+ # __PROJECT_NAME__
2
+
3
+ A production-oriented FastAPI service with JWT authentication, PostgreSQL, Alembic, persistent audit logs, and role-based access control.
4
+
5
+ ## Included
6
+
7
+ - Access tokens plus cookie-only rotating refresh tokens, revocation, and sessions
8
+ - Password reset and email verification
9
+ - Hierarchical roles, permissions, and per-user permissions
10
+ - Structured logs, request IDs, CORS, trusted hosts, rate limiting, and security headers
11
+ - Sync and async SQLAlchemy sessions with configurable pools
12
+ - Liveness, readiness, and database health endpoints
13
+ - Alembic migrations and an idempotent RBAC seed command
14
+ - A non-root Docker image, local Compose stack, tests, Ruff, and CI
15
+
16
+ ## Requirements
17
+
18
+ Python 3.12+, PostgreSQL 14+, and optionally Docker with Compose v2.
19
+
20
+ ## Local setup
21
+
22
+ Create the PostgreSQL database configured by `.env`, then:
23
+
24
+ ```bash
25
+ python3.12 -m venv .venv
26
+ source .venv/bin/activate
27
+ python -m pip install --upgrade pip
28
+ pip install -r requirements-dev.txt
29
+ cp sample.env .env
30
+ alembic upgrade head
31
+ python scripts/seed_first_user.py \
32
+ --email admin@example.com \
33
+ --password 'ChangeMe123!' \
34
+ --full-name 'System Administrator'
35
+ uvicorn app.main:app --reload
36
+ ```
37
+
38
+ On Windows PowerShell, activate with `.venv\\Scripts\\Activate.ps1`. Development API documentation is at `http://localhost:8000/docs`.
39
+
40
+ ## Docker development
41
+
42
+ ```bash
43
+ cp sample.env .env
44
+ docker compose up --build
45
+ ```
46
+
47
+ Seed the administrator from another terminal:
48
+
49
+ ```bash
50
+ docker compose exec api python scripts/seed_first_user.py \
51
+ --email admin@example.com \
52
+ --password 'ChangeMe123!' \
53
+ --full-name 'System Administrator'
54
+ ```
55
+
56
+ The Compose stack is for development; its credentials and bind mount are not production settings.
57
+
58
+ ## Configuration
59
+
60
+ | Setting | Purpose |
61
+ | --- | --- |
62
+ | `APP_ENV` | Use `development`, `test`, or `production` |
63
+ | `SECRET_KEY` | JWT signing key; production requires a unique 32+ character value |
64
+ | `PG_*` | PostgreSQL connection and TLS mode |
65
+ | `ALLOWED_HOSTS` | Comma-separated production hostnames |
66
+ | `ALLOWED_ORIGINS` | Comma-separated browser origins |
67
+ | `USE_REDIS`, `REDIS_URL` | Shared rate limits across replicas |
68
+ | `LOG_TO_FILES`, `LOG_DIR` | Optional rotating files; stdout stays enabled |
69
+ | `MAIL_*`, `FRONTEND_URL` | Email delivery and frontend links |
70
+
71
+ Production startup rejects default/short secrets, wildcard hosts/origins, and non-TLS PostgreSQL modes.
72
+
73
+ ## RBAC
74
+
75
+ Roles contain permissions and may have a parent; users can also receive permissions directly. Codes follow `resource:action`:
76
+
77
+ ```python
78
+ @router.get(
79
+ "/reports",
80
+ dependencies=[Depends(require_permissions("reports:read"))],
81
+ )
82
+ async def list_reports():
83
+ ...
84
+ ```
85
+
86
+ `require_any_permission(...)` accepts any listed permission. Superusers and tokens containing `*` bypass individual checks. The seed command creates `SUPER ADMIN`, `ADMIN`, and `USER`.
87
+
88
+ ## Endpoints
89
+
90
+ - `/api/v1/auth`, `/users`, `/roles`, `/permissions`, and `/audit-logs`
91
+ - `/api/v1/health/live`: process liveness
92
+ - `/api/v1/health/ready`: database readiness; returns `503` when unavailable
93
+
94
+ Docs and OpenAPI are disabled in production.
95
+
96
+ The refresh token is stored only in a scoped HTTP-only cookie. Call
97
+ `POST /api/v1/auth/refresh` with credentials enabled; it returns a new access token
98
+ and rotates the refresh cookie without exposing that token to browser JavaScript.
99
+
100
+ ## Tests
101
+
102
+ ```bash
103
+ pytest
104
+ ruff check .
105
+ ```
106
+
107
+ ## Production checklist
108
+
109
+ 1. Build an immutable image: `docker build -t project-api:1.0.0 .`
110
+ 2. Supply secrets externally; configure explicit HTTPS origins and hosts.
111
+ 3. Use managed PostgreSQL with verified TLS and Redis for multi-replica rate limits.
112
+ 4. Run `alembic upgrade head` once as a release job before new replicas.
113
+ 5. Terminate TLS at a trusted proxy and centralize stdout/stderr logs.
114
+ 6. Monitor liveness/readiness; test backups, restores, rotation, and rollback.
115
+
116
+ ```bash
117
+ docker run --rm --env-file .env project-api:1.0.0 alembic upgrade head
118
+ docker run --rm --env-file .env -p 8000:8000 project-api:1.0.0
119
+ ```
120
+
121
+ Do not run migrations concurrently from every application replica.
@@ -0,0 +1,16 @@
1
+ .git
2
+ .github
3
+ .env
4
+ .env.*
5
+ .venv
6
+ __pycache__
7
+ *.py[cod]
8
+ .pytest_cache
9
+ .ruff_cache
10
+ .coverage
11
+ htmlcov
12
+ logs
13
+ uploads
14
+ tests
15
+ README.md
16
+
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ test:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ cache: pip
19
+ - run: python -m pip install --upgrade pip
20
+ - run: pip install -r requirements-dev.txt
21
+ - run: ruff check .
22
+ - run: pytest
23
+
@@ -0,0 +1,19 @@
1
+ .env
2
+ .env.*
3
+ .venv/
4
+ __pycache__/
5
+ *.py[cod]
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .idea/
10
+ .vscode/
11
+ uploads/*
12
+ !uploads/.gitkeep
13
+
14
+ DENTICON_DATA_FLOW.md
15
+
16
+
17
+ logs/
18
+
19
+ fastapi-forge
@@ -0,0 +1 @@
1
+ Generate migrations after model changes with: alembic revision --autogenerate -m "describe change"
@@ -0,0 +1,51 @@
1
+ from logging.config import fileConfig
2
+
3
+ from alembic import context
4
+ from app.db.base import Base
5
+
6
+ from app.core.config import settings
7
+ from app.db.models import *
8
+
9
+ config = context.config
10
+ config.set_main_option("sqlalchemy.url", settings.postgres_url)
11
+
12
+ if config.config_file_name:
13
+ fileConfig(config.config_file_name)
14
+
15
+ target_metadata = Base.metadata
16
+
17
+
18
+ def run_migrations_offline():
19
+ context.configure(
20
+ url=settings.postgres_url,
21
+ target_metadata=target_metadata,
22
+ include_schemas=True,
23
+ literal_binds=True,
24
+ dialect_opts={"paramstyle": "named"},
25
+ )
26
+ with context.begin_transaction():
27
+ context.run_migrations()
28
+
29
+
30
+ def run_migrations_online():
31
+ from sqlalchemy import engine_from_config, pool
32
+
33
+ connectable = engine_from_config(
34
+ configuration=config.get_section(config.config_ini_section),
35
+ prefix="sqlalchemy.",
36
+ poolclass=pool.NullPool,
37
+ )
38
+ with connectable.connect() as connection:
39
+ context.configure(
40
+ connection=connection,
41
+ target_metadata=target_metadata,
42
+ include_schemas=True,
43
+ )
44
+ with context.begin_transaction():
45
+ context.run_migrations()
46
+
47
+
48
+ if context.is_offline_mode():
49
+ run_migrations_offline()
50
+ else:
51
+ run_migrations_online()
@@ -0,0 +1,28 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}