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,33 @@
1
+ services:
2
+ db:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_DB: project_name
6
+ POSTGRES_USER: postgres
7
+ POSTGRES_PASSWORD: postgres
8
+ healthcheck:
9
+ test: ["CMD-SHELL", "pg_isready -U postgres -d project_name"]
10
+ interval: 5s
11
+ timeout: 5s
12
+ retries: 10
13
+ volumes:
14
+ - postgres-data:/var/lib/postgresql/data
15
+
16
+ api:
17
+ build: .
18
+ env_file: .env
19
+ environment:
20
+ PG_HOST: db
21
+ PG_SSLMODE: disable
22
+ command: ["sh", "-c", "alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"]
23
+ depends_on:
24
+ db:
25
+ condition: service_healthy
26
+ ports:
27
+ - "8000:8000"
28
+ volumes:
29
+ - .:/app
30
+
31
+ volumes:
32
+ postgres-data:
33
+
@@ -0,0 +1,14 @@
1
+ [tool.pytest.ini_options]
2
+ testpaths = ["tests"]
3
+ asyncio_mode = "auto"
4
+ pythonpath = ["."]
5
+
6
+ [tool.ruff]
7
+ target-version = "py312"
8
+ line-length = 100
9
+
10
+ [tool.ruff.lint]
11
+ select = ["E9", "F63", "F7", "F82"]
12
+
13
+ [tool.ruff.lint.per-file-ignores]
14
+ "app/db/models/*.py" = ["F821"]
@@ -0,0 +1,5 @@
1
+ -r requirements.txt
2
+ pytest>=8.0,<9.0
3
+ pytest-asyncio>=0.23,<1.0
4
+ ruff>=0.8,<1.0
5
+
@@ -0,0 +1,16 @@
1
+ alembic>=1.13.0
2
+ fastapi>=0.111.0
3
+ asyncpg>=0.30.0
4
+ psycopg[binary]>=3.2.0
5
+ pydantic-settings>=2.0.0
6
+ pydantic[email]>=2.0.0
7
+ uvicorn[standard]>=0.29.0
8
+ SQLAlchemy>=2.0.30
9
+ python-jose[cryptography]>=3.3.0
10
+ passlib[bcrypt]>=1.7.4
11
+ bcrypt==4.0.1
12
+ python-multipart>=0.0.9
13
+ slowapi>=0.1.9
14
+ redis>=5.0.0
15
+ fastapi-mail>=1.4.1
16
+ structlog>=24.0.0
@@ -0,0 +1,42 @@
1
+ APP_NAME=__PROJECT_NAME__
2
+ APP_VERSION=1.0.0
3
+ APP_ENV=development
4
+ APP_DEBUG=false
5
+ SECRET_KEY=replace-with-a-long-random-secret
6
+
7
+ PG_HOST=localhost
8
+ PG_PORT=5432
9
+ PG_DATABASE=project_name
10
+ PG_USER=postgres
11
+ PG_PASSWORD=postgres
12
+ PG_SSLMODE=prefer
13
+
14
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
15
+ REFRESH_TOKEN_EXPIRE_DAYS=7
16
+ PASSWORD_RESET_TOKEN_EXPIRE_MINUTES=15
17
+ ALGORITHM=HS256
18
+
19
+ ALLOWED_ORIGINS=http://localhost:3000
20
+ ALLOWED_HOSTS=localhost,127.0.0.1
21
+
22
+ USE_REDIS=false
23
+ REDIS_URL=
24
+ RATE_LIMIT_PER_MINUTE=60
25
+ LOG_DIR=logs
26
+ LOG_TO_FILES=false
27
+
28
+ # Database pool tuning
29
+ DATABASE_POOL_SIZE=10
30
+ DATABASE_MAX_OVERFLOW=20
31
+ DATABASE_POOL_TIMEOUT=30
32
+ DATABASE_DEBUG=false
33
+
34
+ MAIL_USERNAME=
35
+ MAIL_PASSWORD=
36
+ MAIL_FROM=noreply@example.com
37
+ MAIL_PORT=587
38
+ MAIL_SERVER=smtp.example.com
39
+ MAIL_STARTTLS=true
40
+ MAIL_SSL_TLS=false
41
+ MAIL_FROM_NAME=__PROJECT_NAME__
42
+ FRONTEND_URL=http://localhost:3000
@@ -0,0 +1,51 @@
1
+ import argparse
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
6
+ if str(PROJECT_ROOT) not in sys.path:
7
+ sys.path.insert(0, str(PROJECT_ROOT))
8
+
9
+ from sqlalchemy import select
10
+
11
+ from app.core.security import hash_password
12
+ from app.db.models import User
13
+ from app.db.session import SyncSessionLocal
14
+
15
+
16
+ def username_from_email(email: str) -> str:
17
+ return email.split("@", 1)[0].replace(".", "_").replace("-", "_").lower()
18
+
19
+
20
+ def main() -> None:
21
+ parser = argparse.ArgumentParser(description="Create the first superuser.")
22
+ parser.add_argument("--email", required=True)
23
+ parser.add_argument("--password", required=True)
24
+ parser.add_argument("--full-name", required=True)
25
+ args = parser.parse_args()
26
+
27
+ with SyncSessionLocal() as session:
28
+ user = session.scalar(
29
+ select(User).where(User.email == args.email.lower())
30
+ )
31
+ if user is None:
32
+ user = User(
33
+ email=args.email.lower(),
34
+ username=username_from_email(args.email),
35
+ full_name=args.full_name,
36
+ hashed_password=hash_password(args.password),
37
+ )
38
+ session.add(user)
39
+ user.full_name = args.full_name
40
+ user.hashed_password = hash_password(args.password)
41
+ user.is_active = True
42
+ user.is_superuser = True
43
+ user.is_verified = True
44
+ user.restore()
45
+ session.commit()
46
+ print(f"SUPERUSER ready: {user.email}")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
51
+
@@ -0,0 +1,42 @@
1
+ from uuid import uuid4
2
+
3
+ import pytest
4
+
5
+ from app.db.models.audit_log import AuditLog
6
+ from app.services.audit import AuditService
7
+
8
+
9
+ class FakeSession:
10
+ def __init__(self):
11
+ self.added = []
12
+ self.flushed = False
13
+
14
+ def add(self, value):
15
+ self.added.append(value)
16
+
17
+ async def flush(self):
18
+ self.flushed = True
19
+
20
+
21
+ @pytest.mark.asyncio
22
+ async def test_audit_service_persists_context():
23
+ session = FakeSession()
24
+ user_id = uuid4()
25
+
26
+ entry = await AuditService(session).log(
27
+ action="users.update",
28
+ resource="users",
29
+ resource_id="target-user",
30
+ user_id=user_id,
31
+ old_values={"is_active": True},
32
+ new_values={"is_active": False},
33
+ metadata={"reason": "offboarding"},
34
+ request_id="request-123",
35
+ )
36
+
37
+ assert isinstance(entry, AuditLog)
38
+ assert entry.user_id == user_id
39
+ assert entry.log_metadata == {"reason": "offboarding"}
40
+ assert entry.request_id == "request-123"
41
+ assert session.added == [entry]
42
+ assert session.flushed
@@ -0,0 +1,27 @@
1
+ import pytest
2
+ from pydantic import ValidationError
3
+
4
+ from app.core.config import Settings
5
+
6
+
7
+ def test_database_urls_escape_credentials_and_enable_tls():
8
+ configured = Settings(pg_password="p@ss", pg_sslmode="require")
9
+
10
+ assert "%40" in configured.postgres_url
11
+ assert "ssl=require" in configured.postgres_async_url
12
+
13
+
14
+ def test_production_rejects_unsafe_defaults():
15
+ with pytest.raises(ValidationError):
16
+ Settings(APP_ENV="production", _env_file=None)
17
+
18
+
19
+ def test_production_accepts_explicit_secure_settings():
20
+ Settings(
21
+ APP_ENV="production",
22
+ SECRET_KEY="x" * 48,
23
+ ALLOWED_HOSTS=["api.example.com"],
24
+ ALLOWED_ORIGINS=["https://example.com"],
25
+ pg_sslmode="verify-full",
26
+ _env_file=None,
27
+ )
@@ -0,0 +1,20 @@
1
+ import ast
2
+ from pathlib import Path
3
+
4
+
5
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
6
+
7
+
8
+ def test_generated_non_rbac_project_structure():
9
+ assert not (PROJECT_ROOT / "app/api/v1/roles").exists()
10
+ assert not (PROJECT_ROOT / "app/api/v1/permissions").exists()
11
+ assert not (PROJECT_ROOT / "app/db/models/permission.py").exists()
12
+ assert (PROJECT_ROOT / "Dockerfile").exists()
13
+ assert (PROJECT_ROOT / ".github/workflows/ci.yml").exists()
14
+
15
+
16
+ def test_python_sources_compile():
17
+ for path in PROJECT_ROOT.rglob("*.py"):
18
+ source = path.read_text(encoding="utf-8")
19
+ ast.parse(source, filename=str(path))
20
+ compile(source, str(path), "exec")
@@ -0,0 +1,68 @@
1
+ from app.core.security import (
2
+ create_access_token,
3
+ create_refresh_token,
4
+ decode_token,
5
+ hash_password,
6
+ hash_opaque_token,
7
+ verify_password,
8
+ )
9
+ from app.api.v1.auth.schema import TokenResponse, UserResponse
10
+
11
+
12
+ def test_password_hash_round_trip():
13
+ password = "StrongPassword123!"
14
+ hashed = hash_password(password)
15
+
16
+ assert hashed != password
17
+ assert verify_password(password, hashed)
18
+ assert not verify_password("wrong-password", hashed)
19
+
20
+
21
+ def test_access_token_round_trip():
22
+ token, _ = create_access_token(
23
+ "user-123",
24
+ {"permissions": ["users:read"], "role": "ADMIN"},
25
+ )
26
+
27
+ payload = decode_token(token, expected_type="access")
28
+
29
+ assert payload["sub"] == "user-123"
30
+ assert payload["permissions"] == ["users:read"]
31
+ assert payload["role"] == "ADMIN"
32
+ assert payload["jti"]
33
+
34
+
35
+ def test_refresh_token_round_trip():
36
+ token, _ = create_refresh_token("user-123")
37
+
38
+ payload = decode_token(token, expected_type="refresh")
39
+
40
+ assert payload["sub"] == "user-123"
41
+ assert payload["type"] == "refresh"
42
+
43
+
44
+ def test_opaque_token_hash_is_deterministic_and_one_way():
45
+ token = "secret-reset-token"
46
+
47
+ assert hash_opaque_token(token) == hash_opaque_token(token)
48
+ assert hash_opaque_token(token) != token
49
+
50
+
51
+ def test_refresh_token_is_excluded_from_serialized_login_response():
52
+ user = UserResponse(
53
+ id="user-123",
54
+ email="user@example.com",
55
+ username="user",
56
+ full_name="Example User",
57
+ is_active=True,
58
+ is_superuser=False,
59
+ is_verified=True,
60
+ )
61
+ response = TokenResponse(
62
+ access_token="access",
63
+ refresh_token="refresh-secret",
64
+ expires_in=1800,
65
+ user=user,
66
+ )
67
+
68
+ assert "refresh_token" not in response.model_dump()
@@ -0,0 +1,225 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastapi-forge-cli
3
+ Version: 0.1.0
4
+ Summary: Generate production-oriented FastAPI projects with optional RBAC.
5
+ Author: FastAPI Forge Contributors
6
+ License-Expression: MIT
7
+ Keywords: fastapi,boilerplate,generator,rbac,cli
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Framework :: FastAPI
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: dev
20
+ Requires-Dist: build<2.0,>=1.2; extra == "dev"
21
+ Requires-Dist: pytest<9.0,>=8.0; extra == "dev"
22
+ Requires-Dist: ruff<1.0,>=0.8; extra == "dev"
23
+ Requires-Dist: twine<7.0,>=5.1; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # FastAPI Forge
27
+
28
+ FastAPI Forge is a zero-runtime-dependency CLI that generates production-oriented FastAPI projects with PostgreSQL, Alembic migrations, JWT authentication, audit logging, and optional role-based access control (RBAC).
29
+
30
+ ## Generated features
31
+
32
+ - Access and rotating refresh tokens, revocation, and session management
33
+ - Password reset and email verification flows
34
+ - PostgreSQL with sync and async SQLAlchemy sessions
35
+ - Alembic migrations and an idempotent first-user seed command
36
+ - Structured logs, request IDs, rate limiting, CORS, trusted hosts, and security headers
37
+ - Liveness, readiness, and database health endpoints
38
+ - A multi-stage, non-root Docker image and local Compose stack
39
+ - Pytest tests, Ruff checks, and GitHub Actions CI
40
+
41
+ The RBAC variant adds hierarchical roles, permissions, per-user permissions, protected administration APIs, and RBAC seed data. The non-RBAC variant uses `is_superuser` for administrative authorization.
42
+
43
+ ## Requirements
44
+
45
+ - Python 3.9+ for the generator
46
+ - Python 3.12+ and PostgreSQL 14+ for generated applications
47
+ - Docker with Compose v2 for the optional container workflow
48
+
49
+ ## Install
50
+
51
+ Install the CLI in an isolated environment from a published package:
52
+
53
+ ```bash
54
+ pipx install fastapi-forge-cli
55
+ ```
56
+
57
+ Or install it into the current Python environment:
58
+
59
+ ```bash
60
+ python -m pip install fastapi-forge-cli
61
+ ```
62
+
63
+ The PyPI distribution is named `fastapi-forge-cli` because `fastapi-forge` is
64
+ already used by another project. The installed terminal command remains
65
+ `fastapi-forge`, and the Python import remains `fastapi_forge`.
66
+
67
+ For development from a source checkout:
68
+
69
+ ```bash
70
+ cd fastapi-forge
71
+ python3 -m venv .venv
72
+ source .venv/bin/activate
73
+ python -m pip install --upgrade pip
74
+ pip install -e .
75
+ ```
76
+
77
+ For generator development, run `pip install -e ".[dev]"`. On Windows PowerShell, activate with `.venv\\Scripts\\Activate.ps1`.
78
+
79
+ ## Generate a project
80
+
81
+ ```bash
82
+ # With RBAC
83
+ fastapi-forge create "Inventory API" --with-rbac
84
+
85
+ # Without RBAC
86
+ fastapi-forge create "Inventory API" --without-rbac
87
+
88
+ # Select a parent directory
89
+ fastapi-forge create "Inventory API" --with-rbac --output-dir ./projects
90
+ ```
91
+
92
+ The same generator is available as a Python library:
93
+
94
+ ```python
95
+ from pathlib import Path
96
+
97
+ from fastapi_forge import create_project
98
+
99
+ project = create_project(
100
+ name="Inventory API",
101
+ output_dir=Path("./projects"),
102
+ with_rbac=True,
103
+ )
104
+ print(project)
105
+ ```
106
+
107
+ Generation is staged before installation at the destination. Existing projects are
108
+ preserved unless `--force` is explicitly supplied, and symbolic-link destinations
109
+ are never replaced.
110
+
111
+ If neither RBAC option is supplied, an interactive terminal asks which variant to use. Non-interactive environments default to RBAC. Existing destinations are protected; `--force` permanently replaces the matching generated-project directory.
112
+
113
+ ```text
114
+ usage: fastapi-forge create [-h] [--output-dir OUTPUT_DIR]
115
+ [--with-rbac | --without-rbac] [--force]
116
+ name
117
+ ```
118
+
119
+ ## Getting help
120
+
121
+ Discover available commands:
122
+
123
+ ```bash
124
+ fastapi-forge --help
125
+ ```
126
+
127
+ See every project-generation option:
128
+
129
+ ```bash
130
+ fastapi-forge create --help
131
+ ```
132
+
133
+ Print copy-ready examples:
134
+
135
+ ```bash
136
+ fastapi-forge examples
137
+ ```
138
+
139
+ Common commands include:
140
+
141
+ ```bash
142
+ fastapi-forge create "Inventory API" --with-rbac
143
+ fastapi-forge create "Public API" --without-rbac
144
+ fastapi-forge create "Billing API" --with-rbac --output-dir ./projects
145
+ ```
146
+
147
+ ## Run a generated project
148
+
149
+ ### Python
150
+
151
+ Create the PostgreSQL database described by `sample.env`, then:
152
+
153
+ ```bash
154
+ cd inventory-api
155
+ python3.12 -m venv .venv
156
+ source .venv/bin/activate
157
+ python -m pip install --upgrade pip
158
+ pip install -r requirements-dev.txt
159
+ cp sample.env .env
160
+ alembic upgrade head
161
+ python scripts/seed_first_user.py \
162
+ --email admin@example.com \
163
+ --password 'ChangeMe123!' \
164
+ --full-name 'System Administrator'
165
+ uvicorn app.main:app --reload
166
+ ```
167
+
168
+ Open `http://localhost:8000/docs`. Run `pytest` and `ruff check .` before committing.
169
+
170
+ ### Docker
171
+
172
+ ```bash
173
+ cd inventory-api
174
+ cp sample.env .env
175
+ docker compose up --build
176
+ ```
177
+
178
+ In another terminal:
179
+
180
+ ```bash
181
+ docker compose exec api python scripts/seed_first_user.py \
182
+ --email admin@example.com \
183
+ --password 'ChangeMe123!' \
184
+ --full-name 'System Administrator'
185
+ ```
186
+
187
+ The Compose file is for local development; it uses development credentials and a bind mount.
188
+
189
+ ## Production deployment checklist
190
+
191
+ 1. Set `APP_ENV=production`.
192
+ 2. Supply a unique `SECRET_KEY` of at least 32 characters from a secret manager.
193
+ 3. Set explicit `ALLOWED_HOSTS` and HTTPS `ALLOWED_ORIGINS` values.
194
+ 4. Use managed PostgreSQL credentials and `PG_SSLMODE=require`, `verify-ca`, or `verify-full`.
195
+ 5. Configure Redis when rate limits must be shared across replicas.
196
+ 6. Configure SMTP and `FRONTEND_URL` for account emails.
197
+ 7. Run `alembic upgrade head` as one release job before starting new replicas.
198
+ 8. Terminate TLS at a trusted load balancer or reverse proxy.
199
+ 9. Send stdout/stderr to centralized logging. Enable file logs only with persistent storage.
200
+ 10. Monitor `/api/v1/health/live` and `/api/v1/health/ready`.
201
+ 11. Back up PostgreSQL, test restores, rotate secrets, and define rollback procedures.
202
+
203
+ Production mode refuses startup with a default/short secret, wildcard hosts or origins, or a non-TLS database mode. Interactive API documentation is disabled.
204
+
205
+ ```bash
206
+ docker build -t inventory-api:1.0.0 .
207
+ docker run --rm --env-file .env inventory-api:1.0.0 alembic upgrade head
208
+ docker run --rm --env-file .env -p 8000:8000 inventory-api:1.0.0
209
+ ```
210
+
211
+ Do not run migrations independently from every application replica.
212
+
213
+ ## Generator development and release
214
+
215
+ ```bash
216
+ pytest
217
+ python -m build
218
+ python -m twine check dist/*
219
+ ```
220
+
221
+ Review generated output from both variants whenever templates change.
222
+
223
+ ## License
224
+
225
+ MIT