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.
- fastapi_forge/__init__.py +7 -0
- fastapi_forge/__main__.py +6 -0
- fastapi_forge/cli.py +211 -0
- fastapi_forge/templates/with_rbac/Dockerfile +31 -0
- fastapi_forge/templates/with_rbac/README.md +121 -0
- fastapi_forge/templates/with_rbac/_dockerignore +16 -0
- fastapi_forge/templates/with_rbac/_github/workflows/ci.yml +23 -0
- fastapi_forge/templates/with_rbac/_gitignore +19 -0
- fastapi_forge/templates/with_rbac/alembic/README +1 -0
- fastapi_forge/templates/with_rbac/alembic/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/alembic/env.py +51 -0
- fastapi_forge/templates/with_rbac/alembic/script.py.mako +28 -0
- fastapi_forge/templates/with_rbac/alembic/versions/2255ba4f9604_fresh_baseline.py +204 -0
- fastapi_forge/templates/with_rbac/alembic.ini +35 -0
- fastapi_forge/templates/with_rbac/app/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/api.py +34 -0
- fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/repository.py +38 -0
- fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/router.py +50 -0
- fastapi_forge/templates/with_rbac/app/api/v1/audit_logs/schema.py +21 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/__init__.py +0 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/repository.py +179 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/router.py +209 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/schema.py +98 -0
- fastapi_forge/templates/with_rbac/app/api/v1/auth/service.py +383 -0
- fastapi_forge/templates/with_rbac/app/api/v1/health/__init__.py +3 -0
- fastapi_forge/templates/with_rbac/app/api/v1/health/router.py +23 -0
- fastapi_forge/templates/with_rbac/app/api/v1/health/schema.py +5 -0
- fastapi_forge/templates/with_rbac/app/api/v1/health/service.py +25 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/__init__.py +0 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/repository.py +80 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/router.py +151 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/schema.py +40 -0
- fastapi_forge/templates/with_rbac/app/api/v1/permissions/service.py +156 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/repository.py +161 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/router.py +169 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/schema.py +51 -0
- fastapi_forge/templates/with_rbac/app/api/v1/roles/service.py +319 -0
- fastapi_forge/templates/with_rbac/app/api/v1/schema.py +7 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/repository.py +181 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/router.py +146 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/schema.py +112 -0
- fastapi_forge/templates/with_rbac/app/api/v1/users/service.py +291 -0
- fastapi_forge/templates/with_rbac/app/core/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/core/config.py +131 -0
- fastapi_forge/templates/with_rbac/app/core/dependencies.py +131 -0
- fastapi_forge/templates/with_rbac/app/core/exceptions.py +162 -0
- fastapi_forge/templates/with_rbac/app/core/logging.py +231 -0
- fastapi_forge/templates/with_rbac/app/core/middleware.py +188 -0
- fastapi_forge/templates/with_rbac/app/core/responses.py +108 -0
- fastapi_forge/templates/with_rbac/app/core/security.py +115 -0
- fastapi_forge/templates/with_rbac/app/db/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/db/base.py +5 -0
- fastapi_forge/templates/with_rbac/app/db/models/__init__.py +16 -0
- fastapi_forge/templates/with_rbac/app/db/models/audit_log.py +58 -0
- fastapi_forge/templates/with_rbac/app/db/models/auth_token.py +72 -0
- fastapi_forge/templates/with_rbac/app/db/models/notification.py +49 -0
- fastapi_forge/templates/with_rbac/app/db/models/permission.py +174 -0
- fastapi_forge/templates/with_rbac/app/db/models/revoked_token.py +21 -0
- fastapi_forge/templates/with_rbac/app/db/models/user.py +53 -0
- fastapi_forge/templates/with_rbac/app/db/schemas/__init__.py +8 -0
- fastapi_forge/templates/with_rbac/app/db/schemas/common.py +70 -0
- fastapi_forge/templates/with_rbac/app/db/schemas/names.py +9 -0
- fastapi_forge/templates/with_rbac/app/db/session.py +86 -0
- fastapi_forge/templates/with_rbac/app/helper/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/helper/pagination_helper.py +44 -0
- fastapi_forge/templates/with_rbac/app/helper/search.py +51 -0
- fastapi_forge/templates/with_rbac/app/helper/sorting.py +77 -0
- fastapi_forge/templates/with_rbac/app/main.py +66 -0
- fastapi_forge/templates/with_rbac/app/repositories/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/repositories/base.py +347 -0
- fastapi_forge/templates/with_rbac/app/services/__init__.py +1 -0
- fastapi_forge/templates/with_rbac/app/services/audit.py +58 -0
- fastapi_forge/templates/with_rbac/app/services/email.py +118 -0
- fastapi_forge/templates/with_rbac/app/services/notification.py +82 -0
- fastapi_forge/templates/with_rbac/app/templates/email/notification.html +7 -0
- fastapi_forge/templates/with_rbac/app/templates/email/password_reset.html +7 -0
- fastapi_forge/templates/with_rbac/app/templates/email/verify_email.html +7 -0
- fastapi_forge/templates/with_rbac/app/templates/email/welcome.html +6 -0
- fastapi_forge/templates/with_rbac/app/utils/casing.py +31 -0
- fastapi_forge/templates/with_rbac/compose.yaml +33 -0
- fastapi_forge/templates/with_rbac/pyproject.toml +14 -0
- fastapi_forge/templates/with_rbac/requirements-dev.txt +5 -0
- fastapi_forge/templates/with_rbac/requirements.txt +16 -0
- fastapi_forge/templates/with_rbac/sample.env +42 -0
- fastapi_forge/templates/with_rbac/scripts/seed_first_user.py +166 -0
- fastapi_forge/templates/with_rbac/tests/test_audit.py +42 -0
- fastapi_forge/templates/with_rbac/tests/test_config.py +27 -0
- fastapi_forge/templates/with_rbac/tests/test_generator.py +20 -0
- fastapi_forge/templates/with_rbac/tests/test_permissions.py +36 -0
- fastapi_forge/templates/with_rbac/tests/test_security.py +68 -0
- fastapi_forge/templates/without_rbac/Dockerfile +31 -0
- fastapi_forge/templates/without_rbac/README.md +106 -0
- fastapi_forge/templates/without_rbac/_dockerignore +16 -0
- fastapi_forge/templates/without_rbac/_github/workflows/ci.yml +23 -0
- fastapi_forge/templates/without_rbac/_gitignore +19 -0
- fastapi_forge/templates/without_rbac/alembic/README +1 -0
- fastapi_forge/templates/without_rbac/alembic/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/alembic/env.py +51 -0
- fastapi_forge/templates/without_rbac/alembic/script.py.mako +28 -0
- fastapi_forge/templates/without_rbac/alembic/versions/2255ba4f9604_fresh_baseline.py +125 -0
- fastapi_forge/templates/without_rbac/alembic.ini +35 -0
- fastapi_forge/templates/without_rbac/app/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/v1/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/v1/api.py +29 -0
- fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/repository.py +38 -0
- fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/router.py +45 -0
- fastapi_forge/templates/without_rbac/app/api/v1/audit_logs/schema.py +21 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/__init__.py +0 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/repository.py +127 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/router.py +207 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/schema.py +96 -0
- fastapi_forge/templates/without_rbac/app/api/v1/auth/service.py +373 -0
- fastapi_forge/templates/without_rbac/app/api/v1/health/__init__.py +3 -0
- fastapi_forge/templates/without_rbac/app/api/v1/health/router.py +23 -0
- fastapi_forge/templates/without_rbac/app/api/v1/health/schema.py +5 -0
- fastapi_forge/templates/without_rbac/app/api/v1/health/service.py +25 -0
- fastapi_forge/templates/without_rbac/app/api/v1/schema.py +7 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/repository.py +69 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/router.py +94 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/schema.py +50 -0
- fastapi_forge/templates/without_rbac/app/api/v1/users/service.py +92 -0
- fastapi_forge/templates/without_rbac/app/core/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/core/config.py +131 -0
- fastapi_forge/templates/without_rbac/app/core/dependencies.py +71 -0
- fastapi_forge/templates/without_rbac/app/core/exceptions.py +162 -0
- fastapi_forge/templates/without_rbac/app/core/logging.py +231 -0
- fastapi_forge/templates/without_rbac/app/core/middleware.py +188 -0
- fastapi_forge/templates/without_rbac/app/core/responses.py +108 -0
- fastapi_forge/templates/without_rbac/app/core/security.py +115 -0
- fastapi_forge/templates/without_rbac/app/db/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/db/base.py +5 -0
- fastapi_forge/templates/without_rbac/app/db/models/__init__.py +10 -0
- fastapi_forge/templates/without_rbac/app/db/models/audit_log.py +58 -0
- fastapi_forge/templates/without_rbac/app/db/models/auth_token.py +72 -0
- fastapi_forge/templates/without_rbac/app/db/models/notification.py +49 -0
- fastapi_forge/templates/without_rbac/app/db/models/revoked_token.py +21 -0
- fastapi_forge/templates/without_rbac/app/db/models/user.py +33 -0
- fastapi_forge/templates/without_rbac/app/db/schemas/__init__.py +8 -0
- fastapi_forge/templates/without_rbac/app/db/schemas/common.py +70 -0
- fastapi_forge/templates/without_rbac/app/db/schemas/names.py +5 -0
- fastapi_forge/templates/without_rbac/app/db/session.py +86 -0
- fastapi_forge/templates/without_rbac/app/helper/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/helper/pagination_helper.py +44 -0
- fastapi_forge/templates/without_rbac/app/helper/search.py +51 -0
- fastapi_forge/templates/without_rbac/app/helper/sorting.py +77 -0
- fastapi_forge/templates/without_rbac/app/main.py +66 -0
- fastapi_forge/templates/without_rbac/app/repositories/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/repositories/base.py +347 -0
- fastapi_forge/templates/without_rbac/app/services/__init__.py +1 -0
- fastapi_forge/templates/without_rbac/app/services/audit.py +58 -0
- fastapi_forge/templates/without_rbac/app/services/email.py +118 -0
- fastapi_forge/templates/without_rbac/app/services/notification.py +82 -0
- fastapi_forge/templates/without_rbac/app/templates/email/notification.html +7 -0
- fastapi_forge/templates/without_rbac/app/templates/email/password_reset.html +7 -0
- fastapi_forge/templates/without_rbac/app/templates/email/verify_email.html +7 -0
- fastapi_forge/templates/without_rbac/app/templates/email/welcome.html +6 -0
- fastapi_forge/templates/without_rbac/app/utils/casing.py +31 -0
- fastapi_forge/templates/without_rbac/compose.yaml +33 -0
- fastapi_forge/templates/without_rbac/pyproject.toml +14 -0
- fastapi_forge/templates/without_rbac/requirements-dev.txt +5 -0
- fastapi_forge/templates/without_rbac/requirements.txt +16 -0
- fastapi_forge/templates/without_rbac/sample.env +42 -0
- fastapi_forge/templates/without_rbac/scripts/seed_first_user.py +51 -0
- fastapi_forge/templates/without_rbac/tests/test_audit.py +42 -0
- fastapi_forge/templates/without_rbac/tests/test_config.py +27 -0
- fastapi_forge/templates/without_rbac/tests/test_generator.py +20 -0
- fastapi_forge/templates/without_rbac/tests/test_security.py +68 -0
- fastapi_forge_cli-0.1.0.dist-info/METADATA +225 -0
- fastapi_forge_cli-0.1.0.dist-info/RECORD +181 -0
- fastapi_forge_cli-0.1.0.dist-info/WHEEL +5 -0
- fastapi_forge_cli-0.1.0.dist-info/entry_points.txt +2 -0
- fastapi_forge_cli-0.1.0.dist-info/licenses/LICENSE +18 -0
- fastapi_forge_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Boolean, DateTime, Text, func
|
|
5
|
+
from sqlalchemy.dialects.postgresql import UUID
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def utcnow():
|
|
10
|
+
return datetime.now(timezone.utc)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UUIDMixin:
|
|
14
|
+
"""Provides a UUID primary key."""
|
|
15
|
+
|
|
16
|
+
id: Mapped[uuid.UUID] = mapped_column(
|
|
17
|
+
UUID(as_uuid=True),
|
|
18
|
+
primary_key=True,
|
|
19
|
+
default=uuid.uuid4,
|
|
20
|
+
index=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TimestampMixin:
|
|
25
|
+
"""Adds created_at / updated_at columns with automatic server defaults."""
|
|
26
|
+
|
|
27
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
28
|
+
DateTime(timezone=True),
|
|
29
|
+
server_default=func.now(),
|
|
30
|
+
nullable=False,
|
|
31
|
+
)
|
|
32
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
33
|
+
DateTime(timezone=True),
|
|
34
|
+
server_default=func.now(),
|
|
35
|
+
onupdate=func.now(),
|
|
36
|
+
nullable=False,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SoftDeleteMixin:
|
|
41
|
+
"""
|
|
42
|
+
Provides soft-delete support.
|
|
43
|
+
- is_deleted=True → soft-deleted (row hidden from normal queries)
|
|
44
|
+
- deleted_at → timestamp of soft-deletion
|
|
45
|
+
Call .hard_delete() on the session to permanently remove the row.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
is_deleted: Mapped[bool] = mapped_column(
|
|
49
|
+
Boolean, default=False, nullable=False, index=True
|
|
50
|
+
)
|
|
51
|
+
deleted_at: Mapped[datetime | None] = mapped_column(
|
|
52
|
+
DateTime(timezone=True), nullable=True
|
|
53
|
+
)
|
|
54
|
+
deletion_note: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
55
|
+
|
|
56
|
+
def soft_delete(self, note: str | None = None) -> None:
|
|
57
|
+
self.is_deleted = True
|
|
58
|
+
self.deleted_at = utcnow()
|
|
59
|
+
self.deletion_note = note
|
|
60
|
+
|
|
61
|
+
def restore(self) -> None:
|
|
62
|
+
self.is_deleted = False
|
|
63
|
+
self.deleted_at = None
|
|
64
|
+
self.deletion_note = None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class BaseModelMixin(UUIDMixin, TimestampMixin, SoftDeleteMixin):
|
|
68
|
+
"""Convenience mixin that combines UUID PK, timestamps, and soft-delete."""
|
|
69
|
+
|
|
70
|
+
pass
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from sqlalchemy import create_engine, text
|
|
2
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
3
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
4
|
+
|
|
5
|
+
from app.core.config import settings
|
|
6
|
+
from app.core.logging import get_logger
|
|
7
|
+
from app.db.base import Base
|
|
8
|
+
from app.db.models import *
|
|
9
|
+
from app.db.schemas.names import ALL_SCHEMAS
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
async_engine = create_async_engine(
|
|
14
|
+
settings.postgres_async_url,
|
|
15
|
+
echo=settings.database_debug,
|
|
16
|
+
pool_size=settings.database_pool_size,
|
|
17
|
+
max_overflow=settings.database_max_overflow,
|
|
18
|
+
pool_timeout=settings.database_pool_timeout,
|
|
19
|
+
pool_pre_ping=True,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
AsyncSessionLocal: async_sessionmaker[AsyncSession] = async_sessionmaker(
|
|
23
|
+
bind=async_engine,
|
|
24
|
+
class_=AsyncSession,
|
|
25
|
+
autoflush=False,
|
|
26
|
+
autocommit=False,
|
|
27
|
+
expire_on_commit=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
sync_engine = create_engine(
|
|
31
|
+
settings.postgres_url,
|
|
32
|
+
echo=settings.database_debug,
|
|
33
|
+
pool_size=settings.database_pool_size,
|
|
34
|
+
max_overflow=settings.database_max_overflow,
|
|
35
|
+
pool_timeout=settings.database_pool_timeout,
|
|
36
|
+
pool_pre_ping=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
SyncSessionLocal: sessionmaker[Session] = sessionmaker(
|
|
40
|
+
bind=sync_engine,
|
|
41
|
+
class_=Session,
|
|
42
|
+
autoflush=False,
|
|
43
|
+
autocommit=False,
|
|
44
|
+
expire_on_commit=False,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
engine = sync_engine
|
|
48
|
+
SessionLocal = SyncSessionLocal
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_db_and_tables() -> None:
|
|
52
|
+
with sync_engine.begin() as connection:
|
|
53
|
+
for schema in ALL_SCHEMAS:
|
|
54
|
+
connection.execute(text(f"CREATE SCHEMA IF NOT EXISTS {schema}"))
|
|
55
|
+
Base.metadata.create_all(bind=connection)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_session():
|
|
59
|
+
db = SessionLocal()
|
|
60
|
+
try:
|
|
61
|
+
yield db
|
|
62
|
+
finally:
|
|
63
|
+
db.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def check_db_health() -> dict:
|
|
67
|
+
try:
|
|
68
|
+
async with AsyncSessionLocal() as session:
|
|
69
|
+
await session.execute(text("SELECT 1"))
|
|
70
|
+
return {"status": "healthy", "database": "connected"}
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
logger.error("Database health check failed", error=str(exc))
|
|
73
|
+
return {"status": "unhealthy", "database": "disconnected", "error": str(exc)}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def connect_db() -> None:
|
|
77
|
+
result = await check_db_health()
|
|
78
|
+
if result["status"] != "healthy":
|
|
79
|
+
raise RuntimeError("Database connection failed on startup")
|
|
80
|
+
logger.info("Database connected successfully")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def disconnect_db() -> None:
|
|
84
|
+
await async_engine.dispose()
|
|
85
|
+
sync_engine.dispose()
|
|
86
|
+
logger.info("Database connections closed")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from fastapi import Query
|
|
2
|
+
|
|
3
|
+
from app.core.config import settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_pagination_offset(page: int, page_size: int) -> int:
|
|
7
|
+
return (page - 1) * page_size
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def apply_pagination(query, page: int, page_size: int, enabled: bool = True):
|
|
11
|
+
if not enabled:
|
|
12
|
+
return query
|
|
13
|
+
return query.offset(get_pagination_offset(page, page_size)).limit(page_size)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PaginationParams:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
page: int = Query(default=1, ge=1),
|
|
20
|
+
page_size: int = Query(
|
|
21
|
+
default=settings.default_page_size,
|
|
22
|
+
ge=1,
|
|
23
|
+
le=settings.max_page_size,
|
|
24
|
+
alias="pageSize",
|
|
25
|
+
),
|
|
26
|
+
pagination: bool = Query(default=True),
|
|
27
|
+
search: str | None = Query(default=None),
|
|
28
|
+
sort_by: str | None = Query(default=None, alias="sortBy"),
|
|
29
|
+
sort_order: str | None = Query(default="asc", alias="sortOrder"),
|
|
30
|
+
):
|
|
31
|
+
self.page = page
|
|
32
|
+
self.page_size = page_size
|
|
33
|
+
self.pagination = pagination
|
|
34
|
+
self.search = search
|
|
35
|
+
self.sort_by = sort_by
|
|
36
|
+
self.sort_order = sort_order
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def offset(self) -> int:
|
|
40
|
+
return get_pagination_offset(self.page, self.page_size)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def limit(self) -> int:
|
|
44
|
+
return self.page_size
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from sqlalchemy import Text, and_, or_
|
|
2
|
+
from sqlalchemy.sql.sqltypes import String
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
DEFAULT_EXCLUDED_FIELDS = {
|
|
6
|
+
"hashed_password",
|
|
7
|
+
"request_sql",
|
|
8
|
+
"request_params_json",
|
|
9
|
+
"response_json",
|
|
10
|
+
"raw_response_json",
|
|
11
|
+
"coverages_json",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def text_search_filter(model, search: str | None, columns=None, name_columns=None):
|
|
16
|
+
query = (search or "").strip()
|
|
17
|
+
if not query:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
fields = columns or [
|
|
21
|
+
column
|
|
22
|
+
for column in model.__table__.columns
|
|
23
|
+
if isinstance(column.type, (String, Text))
|
|
24
|
+
and column.key not in DEFAULT_EXCLUDED_FIELDS
|
|
25
|
+
]
|
|
26
|
+
expressions = []
|
|
27
|
+
if fields:
|
|
28
|
+
like = f"%{query}%"
|
|
29
|
+
expressions.extend(field.ilike(like) for field in fields)
|
|
30
|
+
|
|
31
|
+
if name_columns:
|
|
32
|
+
first_name_column, last_name_column = name_columns
|
|
33
|
+
parts = query.split(maxsplit=1)
|
|
34
|
+
if len(parts) == 2:
|
|
35
|
+
expressions.extend(
|
|
36
|
+
[
|
|
37
|
+
and_(
|
|
38
|
+
first_name_column.ilike(f"%{parts[0]}%"),
|
|
39
|
+
last_name_column.ilike(f"%{parts[1]}%"),
|
|
40
|
+
),
|
|
41
|
+
and_(
|
|
42
|
+
first_name_column.ilike(f"%{parts[1]}%"),
|
|
43
|
+
last_name_column.ilike(f"%{parts[0]}%"),
|
|
44
|
+
),
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if not expressions:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
return or_(*expressions)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
from sqlalchemy import inspect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_snake_case(value: str) -> str:
|
|
9
|
+
value = value.strip()
|
|
10
|
+
value = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", value)
|
|
11
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", value).lower()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def sort_expressions(
|
|
15
|
+
model: Any,
|
|
16
|
+
sort_by: str | None,
|
|
17
|
+
sort_order: str | None,
|
|
18
|
+
default_order: tuple[Any, ...] = (),
|
|
19
|
+
) -> tuple[Any, ...]:
|
|
20
|
+
if not sort_by:
|
|
21
|
+
return default_order
|
|
22
|
+
|
|
23
|
+
field = to_snake_case(sort_by)
|
|
24
|
+
columns = inspect(model).columns
|
|
25
|
+
if field not in columns:
|
|
26
|
+
raise HTTPException(status_code=422, detail=f"Invalid sortBy '{sort_by}'")
|
|
27
|
+
|
|
28
|
+
column = columns[field]
|
|
29
|
+
direction = _normalize_sort_order(sort_order)
|
|
30
|
+
return (column.desc() if direction == "desc" else column.asc(),)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def sort_items(
|
|
34
|
+
items: list[Any], sort_by: str | None, sort_order: str | None
|
|
35
|
+
) -> list[Any]:
|
|
36
|
+
if not sort_by:
|
|
37
|
+
return items
|
|
38
|
+
|
|
39
|
+
field = to_snake_case(sort_by)
|
|
40
|
+
direction = _normalize_sort_order(sort_order)
|
|
41
|
+
|
|
42
|
+
if items:
|
|
43
|
+
first = items[0]
|
|
44
|
+
if isinstance(first, dict):
|
|
45
|
+
if sort_by not in first and field not in first:
|
|
46
|
+
raise HTTPException(
|
|
47
|
+
status_code=422,
|
|
48
|
+
detail=f"Invalid sortBy '{sort_by}'",
|
|
49
|
+
)
|
|
50
|
+
elif not hasattr(first, field):
|
|
51
|
+
raise HTTPException(status_code=422, detail=f"Invalid sortBy '{sort_by}'")
|
|
52
|
+
|
|
53
|
+
return sorted(
|
|
54
|
+
items,
|
|
55
|
+
key=lambda item: _item_value(item, sort_by, field),
|
|
56
|
+
reverse=direction == "desc",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize_sort_order(sort_order: str | None) -> str:
|
|
61
|
+
value = (sort_order or "asc").lower()
|
|
62
|
+
if value not in {"asc", "desc"}:
|
|
63
|
+
raise HTTPException(
|
|
64
|
+
status_code=422,
|
|
65
|
+
detail="sortOrder must be either 'asc' or 'desc'",
|
|
66
|
+
)
|
|
67
|
+
return value
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _item_value(item, sort_by: str, field: str):
|
|
71
|
+
if isinstance(item, dict):
|
|
72
|
+
value = item.get(sort_by)
|
|
73
|
+
if value is None:
|
|
74
|
+
value = item.get(field)
|
|
75
|
+
else:
|
|
76
|
+
value = getattr(item, field, None)
|
|
77
|
+
return (value is None, value)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from slowapi.errors import RateLimitExceeded
|
|
6
|
+
|
|
7
|
+
from app.api.v1.api import register_api_routes
|
|
8
|
+
from app.core.config import settings
|
|
9
|
+
from app.core.exceptions import register_exception_handlers
|
|
10
|
+
from app.core.logging import configure_logging, get_logger
|
|
11
|
+
from app.core.middleware import limiter, register_middleware
|
|
12
|
+
from app.core.responses import error_response
|
|
13
|
+
from app.db.session import connect_db, disconnect_db
|
|
14
|
+
|
|
15
|
+
configure_logging()
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@asynccontextmanager
|
|
20
|
+
async def lifespan(app: FastAPI):
|
|
21
|
+
logger.info("Starting application", name=settings.APP_NAME, env=settings.APP_ENV)
|
|
22
|
+
await connect_db()
|
|
23
|
+
yield
|
|
24
|
+
await disconnect_db()
|
|
25
|
+
logger.info("Application shutdown complete")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
app = FastAPI(
|
|
29
|
+
title=settings.APP_NAME,
|
|
30
|
+
version=settings.APP_VERSION,
|
|
31
|
+
description="FastAPI boilerplate with JWT authentication, superuser authorization, and audit logs.",
|
|
32
|
+
docs_url="/docs" if not settings.is_production else None,
|
|
33
|
+
redoc_url="/redoc" if not settings.is_production else None,
|
|
34
|
+
openapi_url="/openapi.json" if not settings.is_production else None,
|
|
35
|
+
lifespan=lifespan,
|
|
36
|
+
swagger_ui_parameters={"docExpansion": "none"},
|
|
37
|
+
)
|
|
38
|
+
app.state.limiter = limiter
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.exception_handler(RateLimitExceeded)
|
|
42
|
+
async def rate_limit_handler(request: Request, exc: RateLimitExceeded) -> JSONResponse:
|
|
43
|
+
return JSONResponse(
|
|
44
|
+
status_code=429,
|
|
45
|
+
content=error_response(
|
|
46
|
+
message="Too many requests. Please slow down.",
|
|
47
|
+
error_code="RATE_LIMIT_EXCEEDED",
|
|
48
|
+
request_id=getattr(request.state, "request_id", None),
|
|
49
|
+
).model_dump(by_alias=True),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
register_middleware(app)
|
|
54
|
+
register_exception_handlers(app)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.get("/", tags=["Root"])
|
|
58
|
+
async def root():
|
|
59
|
+
return {
|
|
60
|
+
"message": f"Welcome to {settings.APP_NAME}",
|
|
61
|
+
"version": settings.APP_VERSION,
|
|
62
|
+
"docs": None if settings.is_production else "/docs",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
register_api_routes(app)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|