forgeapi 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 +15 -0
- fastapi_forge/cli.py +125 -0
- fastapi_forge/commands/__init__.py +5 -0
- fastapi_forge/commands/create.py +107 -0
- fastapi_forge/generator.py +300 -0
- fastapi_forge/models.py +187 -0
- fastapi_forge/prompts.py +364 -0
- fastapi_forge/templates/base/.dockerignore.jinja +20 -0
- fastapi_forge/templates/base/.github/workflows/ci.yml.jinja +155 -0
- fastapi_forge/templates/base/.gitignore.jinja +68 -0
- fastapi_forge/templates/base/Dockerfile.jinja +69 -0
- fastapi_forge/templates/base/README.md.jinja +141 -0
- fastapi_forge/templates/base/alembic/README.md.jinja +43 -0
- fastapi_forge/templates/base/alembic/env.py.jinja +93 -0
- fastapi_forge/templates/base/alembic/script.py.mako.jinja +26 -0
- fastapi_forge/templates/base/alembic/versions/.gitkeep.jinja +1 -0
- fastapi_forge/templates/base/alembic.ini.jinja +70 -0
- fastapi_forge/templates/base/app/__init__.py.jinja +5 -0
- fastapi_forge/templates/base/app/api/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/api/auth_api.py.jinja +72 -0
- fastapi_forge/templates/base/app/api/health_api.py.jinja +41 -0
- fastapi_forge/templates/base/app/config/__init__.py.jinja +31 -0
- fastapi_forge/templates/base/app/config/base.py.jinja +52 -0
- fastapi_forge/templates/base/app/config/env.py.jinja +75 -0
- fastapi_forge/templates/base/app/core/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/core/auth.py.jinja +96 -0
- fastapi_forge/templates/base/app/core/config.py.jinja +56 -0
- fastapi_forge/templates/base/app/core/database.py.jinja +68 -0
- fastapi_forge/templates/base/app/core/deps.py.jinja +55 -0
- fastapi_forge/templates/base/app/core/redis.py.jinja +41 -0
- fastapi_forge/templates/base/app/daos/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/exceptions/__init__.py.jinja +7 -0
- fastapi_forge/templates/base/app/exceptions/exception.py.jinja +34 -0
- fastapi_forge/templates/base/app/exceptions/handler.py.jinja +56 -0
- fastapi_forge/templates/base/app/main.py.jinja +24 -0
- fastapi_forge/templates/base/app/models/__init__.py.jinja +7 -0
- fastapi_forge/templates/base/app/models/base.py.jinja +13 -0
- fastapi_forge/templates/base/app/schemas/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/schemas/api_schema.py.jinja +20 -0
- fastapi_forge/templates/base/app/schemas/auth_schema.py.jinja +21 -0
- fastapi_forge/templates/base/app/server.py.jinja +99 -0
- fastapi_forge/templates/base/app/services/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/utils/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/app/utils/log.py.jinja +21 -0
- fastapi_forge/templates/base/config.yaml.jinja +71 -0
- fastapi_forge/templates/base/docker-compose.yml.jinja +117 -0
- fastapi_forge/templates/base/pyproject.toml.jinja +86 -0
- fastapi_forge/templates/base/pytest.ini.jinja +10 -0
- fastapi_forge/templates/base/ruff.toml.jinja +33 -0
- fastapi_forge/templates/base/tests/__init__.py.jinja +3 -0
- fastapi_forge/templates/base/tests/conftest.py.jinja +31 -0
- fastapi_forge/templates/base/tests/test_health.py.jinja +24 -0
- forgeapi-0.1.0.dist-info/METADATA +182 -0
- forgeapi-0.1.0.dist-info/RECORD +57 -0
- forgeapi-0.1.0.dist-info/WHEEL +4 -0
- forgeapi-0.1.0.dist-info/entry_points.txt +2 -0
- forgeapi-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication API
|
|
3
|
+
|
|
4
|
+
This module provides authentication endpoints for user login/logout.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Depends, status
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
|
12
|
+
{% if use_database %}
|
|
13
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
14
|
+
|
|
15
|
+
from app.core.deps import get_db_session
|
|
16
|
+
{% endif %}
|
|
17
|
+
from app.core.config import get_config
|
|
18
|
+
from app.core.auth import AuthHandler
|
|
19
|
+
from app.schemas.auth_schema import TokenResponse
|
|
20
|
+
from app.utils.log import log
|
|
21
|
+
|
|
22
|
+
config = get_config()
|
|
23
|
+
authAPI = APIRouter(prefix=f"{config.app.api}/auth")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@authAPI.post("/login", response_model=TokenResponse)
|
|
27
|
+
async def login(
|
|
28
|
+
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
|
|
29
|
+
{% if use_database %}
|
|
30
|
+
db_session: AsyncSession = Depends(get_db_session),
|
|
31
|
+
{% endif %}
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
User login endpoint.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
form_data: OAuth2 password request form with username and password
|
|
38
|
+
{% if use_database %}
|
|
39
|
+
db_session: Database session
|
|
40
|
+
{% endif %}
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
JWT access token on successful authentication
|
|
44
|
+
"""
|
|
45
|
+
# TODO: Implement actual authentication logic
|
|
46
|
+
# This is a placeholder - replace with real user validation
|
|
47
|
+
|
|
48
|
+
# For demo purposes, accept any credentials
|
|
49
|
+
log.info(f"Login attempt for user: {form_data.username}")
|
|
50
|
+
|
|
51
|
+
# Generate access token
|
|
52
|
+
access_token = AuthHandler.create_access_token({"sub": form_data.username})
|
|
53
|
+
|
|
54
|
+
return TokenResponse(
|
|
55
|
+
access_token=access_token,
|
|
56
|
+
token_type="bearer",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@authAPI.post("/logout")
|
|
61
|
+
async def logout():
|
|
62
|
+
"""
|
|
63
|
+
User logout endpoint.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Success message
|
|
67
|
+
"""
|
|
68
|
+
# TODO: Implement token invalidation if needed
|
|
69
|
+
return JSONResponse(
|
|
70
|
+
status_code=status.HTTP_200_OK,
|
|
71
|
+
content={"message": "Logged out successfully"},
|
|
72
|
+
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Health Check API
|
|
3
|
+
|
|
4
|
+
This module provides health check endpoints for monitoring.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
|
|
9
|
+
from app.core.config import get_config
|
|
10
|
+
from app.schemas.api_schema import HealthResponse
|
|
11
|
+
|
|
12
|
+
config = get_config()
|
|
13
|
+
healthAPI = APIRouter(prefix=f"{config.app.api}/health")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@healthAPI.get("", response_model=HealthResponse)
|
|
17
|
+
async def health_check():
|
|
18
|
+
"""
|
|
19
|
+
Health check endpoint.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Health status response
|
|
23
|
+
"""
|
|
24
|
+
return HealthResponse(
|
|
25
|
+
status="healthy",
|
|
26
|
+
version=config.app.version,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@healthAPI.get("/ready", response_model=HealthResponse)
|
|
31
|
+
async def readiness_check():
|
|
32
|
+
"""
|
|
33
|
+
Readiness check endpoint.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Readiness status response
|
|
37
|
+
"""
|
|
38
|
+
return HealthResponse(
|
|
39
|
+
status="ready",
|
|
40
|
+
version=config.app.version,
|
|
41
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Module
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from app.config.base import BaseConfig, DevConfig, ProdConfig
|
|
6
|
+
from app.config.env import AppConfig
|
|
7
|
+
{% if use_database %}
|
|
8
|
+
from app.config.env import DatabaseConfig
|
|
9
|
+
{% endif %}
|
|
10
|
+
{% if use_redis %}
|
|
11
|
+
from app.config.env import RedisConfig
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% if use_auth %}
|
|
14
|
+
from app.config.env import JwtConfig
|
|
15
|
+
{% endif %}
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BaseConfig",
|
|
19
|
+
"DevConfig",
|
|
20
|
+
"ProdConfig",
|
|
21
|
+
"AppConfig",
|
|
22
|
+
{% if use_database %}
|
|
23
|
+
"DatabaseConfig",
|
|
24
|
+
{% endif %}
|
|
25
|
+
{% if use_redis %}
|
|
26
|
+
"RedisConfig",
|
|
27
|
+
{% endif %}
|
|
28
|
+
{% if use_auth %}
|
|
29
|
+
"JwtConfig",
|
|
30
|
+
{% endif %}
|
|
31
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base Configuration Classes
|
|
3
|
+
|
|
4
|
+
This module defines the base configuration classes for different environments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic_settings import BaseSettings
|
|
8
|
+
|
|
9
|
+
from app.config.env import AppConfig
|
|
10
|
+
{% if use_database %}
|
|
11
|
+
from app.config.env import DatabaseConfig
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% if use_redis %}
|
|
14
|
+
from app.config.env import RedisConfig
|
|
15
|
+
{% endif %}
|
|
16
|
+
{% if use_auth %}
|
|
17
|
+
from app.config.env import JwtConfig
|
|
18
|
+
{% endif %}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BaseConfig(BaseSettings):
|
|
22
|
+
"""Base configuration for all environments."""
|
|
23
|
+
|
|
24
|
+
env: str
|
|
25
|
+
app: AppConfig
|
|
26
|
+
{% if use_database %}
|
|
27
|
+
db: DatabaseConfig
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% if use_redis %}
|
|
30
|
+
redis: RedisConfig
|
|
31
|
+
{% endif %}
|
|
32
|
+
{% if use_auth %}
|
|
33
|
+
jwt: JwtConfig
|
|
34
|
+
{% endif %}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DevConfig(BaseConfig):
|
|
38
|
+
"""Development environment configuration."""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class StagingConfig(BaseConfig):
|
|
44
|
+
"""Staging environment configuration."""
|
|
45
|
+
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ProdConfig(BaseConfig):
|
|
50
|
+
"""Production environment configuration."""
|
|
51
|
+
|
|
52
|
+
pass
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment Configuration Classes
|
|
3
|
+
|
|
4
|
+
This module defines Pydantic models for environment-specific configuration.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AppConfig(BaseModel):
|
|
11
|
+
"""Application configuration."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
description: str = ""
|
|
15
|
+
api: str = "/api"
|
|
16
|
+
host: str = "0.0.0.0"
|
|
17
|
+
port: int = 8000
|
|
18
|
+
uvicorn: str = "app.server:app"
|
|
19
|
+
version: str = "0.1.0"
|
|
20
|
+
reload: bool = True
|
|
21
|
+
|
|
22
|
+
{% if use_database %}
|
|
23
|
+
|
|
24
|
+
class DatabaseConfig(BaseModel):
|
|
25
|
+
"""Database configuration."""
|
|
26
|
+
|
|
27
|
+
host: str
|
|
28
|
+
port: int
|
|
29
|
+
user: str
|
|
30
|
+
password: str
|
|
31
|
+
database: str
|
|
32
|
+
driver_name: str = "postgresql+asyncpg"
|
|
33
|
+
echo: bool = False
|
|
34
|
+
pool_size: int = 5
|
|
35
|
+
max_overflow: int = 10
|
|
36
|
+
pool_recycle: int = 3600
|
|
37
|
+
pool_timeout: int = 30
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def database_url(self) -> str:
|
|
41
|
+
"""Get the database URL."""
|
|
42
|
+
{% if use_sqlite %}
|
|
43
|
+
if "sqlite" in self.driver_name:
|
|
44
|
+
return f"{self.driver_name}:///{self.database}"
|
|
45
|
+
{% endif %}
|
|
46
|
+
return f"{self.driver_name}://{self.user}:{self.password}@{self.host}:{self.port}/{self.database}"
|
|
47
|
+
{% endif %}
|
|
48
|
+
|
|
49
|
+
{% if use_redis %}
|
|
50
|
+
|
|
51
|
+
class RedisConfig(BaseModel):
|
|
52
|
+
"""Redis configuration."""
|
|
53
|
+
|
|
54
|
+
host: str = "localhost"
|
|
55
|
+
port: int = 6379
|
|
56
|
+
db: int = 0
|
|
57
|
+
password: str = ""
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def redis_url(self) -> str:
|
|
61
|
+
"""Get the Redis URL."""
|
|
62
|
+
if self.password:
|
|
63
|
+
return f"redis://:{self.password}@{self.host}:{self.port}/{self.db}"
|
|
64
|
+
return f"redis://{self.host}:{self.port}/{self.db}"
|
|
65
|
+
{% endif %}
|
|
66
|
+
|
|
67
|
+
{% if use_auth %}
|
|
68
|
+
|
|
69
|
+
class JwtConfig(BaseModel):
|
|
70
|
+
"""JWT configuration."""
|
|
71
|
+
|
|
72
|
+
secret_key: str
|
|
73
|
+
algorithm: str = "HS256"
|
|
74
|
+
expire_minutes: int = 1440
|
|
75
|
+
{% endif %}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication Handler
|
|
3
|
+
|
|
4
|
+
This module provides JWT token creation and validation utilities.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
|
|
9
|
+
import jwt
|
|
10
|
+
from passlib.context import CryptContext
|
|
11
|
+
|
|
12
|
+
from app.core.config import get_config
|
|
13
|
+
|
|
14
|
+
config = get_config()
|
|
15
|
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthHandler:
|
|
19
|
+
"""Authentication handler for JWT operations."""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def hash_password(password: str) -> str:
|
|
23
|
+
"""
|
|
24
|
+
Hash a password using bcrypt.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
password: Plain text password
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Hashed password
|
|
31
|
+
"""
|
|
32
|
+
return pwd_context.hash(password)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
|
36
|
+
"""
|
|
37
|
+
Verify a password against its hash.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
plain_password: Plain text password
|
|
41
|
+
hashed_password: Hashed password
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
True if password matches, False otherwise
|
|
45
|
+
"""
|
|
46
|
+
return pwd_context.verify(plain_password, hashed_password)
|
|
47
|
+
|
|
48
|
+
@staticmethod
|
|
49
|
+
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
|
|
50
|
+
"""
|
|
51
|
+
Create a JWT access token.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
data: Data to encode in the token
|
|
55
|
+
expires_delta: Optional expiration time delta
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Encoded JWT token
|
|
59
|
+
"""
|
|
60
|
+
to_encode = data.copy()
|
|
61
|
+
|
|
62
|
+
if expires_delta:
|
|
63
|
+
expire = datetime.now(timezone.utc) + expires_delta
|
|
64
|
+
else:
|
|
65
|
+
expire = datetime.now(timezone.utc) + timedelta(
|
|
66
|
+
minutes=config.jwt.expire_minutes
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
to_encode.update({"exp": expire})
|
|
70
|
+
encoded_jwt = jwt.encode(
|
|
71
|
+
to_encode,
|
|
72
|
+
config.jwt.secret_key,
|
|
73
|
+
algorithm=config.jwt.algorithm,
|
|
74
|
+
)
|
|
75
|
+
return encoded_jwt
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def decode_token(token: str) -> dict | None:
|
|
79
|
+
"""
|
|
80
|
+
Decode a JWT token.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
token: JWT token to decode
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Decoded token data or None if invalid
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
payload = jwt.decode(
|
|
90
|
+
token,
|
|
91
|
+
config.jwt.secret_key,
|
|
92
|
+
algorithms=[config.jwt.algorithm],
|
|
93
|
+
)
|
|
94
|
+
return payload
|
|
95
|
+
except jwt.PyJWTError:
|
|
96
|
+
return None
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration Loader
|
|
3
|
+
|
|
4
|
+
This module handles loading configuration from YAML files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from app.config.base import BaseConfig, DevConfig, StagingConfig, ProdConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_project_path() -> Path:
|
|
16
|
+
"""Get the project root path."""
|
|
17
|
+
return Path(__file__).parent.parent.parent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@lru_cache()
|
|
21
|
+
def get_config(config_file: str = "config.yaml", env: str | None = None) -> BaseConfig:
|
|
22
|
+
"""
|
|
23
|
+
Get configuration for the specified environment.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config_file: Path to the configuration file
|
|
27
|
+
env: Environment name (dev, staging, prod)
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Configuration object for the specified environment
|
|
31
|
+
"""
|
|
32
|
+
project_path = get_project_path()
|
|
33
|
+
config_path = project_path / config_file
|
|
34
|
+
|
|
35
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
36
|
+
yaml_config = yaml.safe_load(f)
|
|
37
|
+
|
|
38
|
+
# Get environment from config or parameter
|
|
39
|
+
if env:
|
|
40
|
+
yaml_config["env"] = env
|
|
41
|
+
else:
|
|
42
|
+
env = yaml_config.get("env", "dev")
|
|
43
|
+
|
|
44
|
+
# Get environment-specific config
|
|
45
|
+
env_config = yaml_config.get(env, {})
|
|
46
|
+
env_config["env"] = env
|
|
47
|
+
|
|
48
|
+
# Select configuration class based on environment
|
|
49
|
+
config_classes = {
|
|
50
|
+
"dev": DevConfig,
|
|
51
|
+
"staging": StagingConfig,
|
|
52
|
+
"prod": ProdConfig,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
config_class = config_classes.get(env, DevConfig)
|
|
56
|
+
return config_class(**env_config)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Utilities
|
|
3
|
+
|
|
4
|
+
This module provides database connection and session management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from sqlalchemy.ext.asyncio import (
|
|
8
|
+
AsyncEngine,
|
|
9
|
+
AsyncSession,
|
|
10
|
+
async_sessionmaker,
|
|
11
|
+
create_async_engine,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from app.config.env import DatabaseConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DatabaseUtil:
|
|
18
|
+
"""Database utility class."""
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
async def init_engine(cls, config: DatabaseConfig) -> AsyncEngine:
|
|
22
|
+
"""
|
|
23
|
+
Initialize async database engine.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Database configuration
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Async database engine
|
|
30
|
+
"""
|
|
31
|
+
engine = create_async_engine(
|
|
32
|
+
config.database_url,
|
|
33
|
+
echo=config.echo,
|
|
34
|
+
pool_size=config.pool_size,
|
|
35
|
+
max_overflow=config.max_overflow,
|
|
36
|
+
pool_recycle=config.pool_recycle,
|
|
37
|
+
pool_timeout=config.pool_timeout,
|
|
38
|
+
)
|
|
39
|
+
return engine
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
async def close_engine(cls, engine: AsyncEngine) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Close database engine.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
engine: Async database engine to close
|
|
48
|
+
"""
|
|
49
|
+
await engine.dispose()
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
async def get_session(cls, engine: AsyncEngine):
|
|
53
|
+
"""
|
|
54
|
+
Get async database session.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
engine: Async database engine
|
|
58
|
+
|
|
59
|
+
Yields:
|
|
60
|
+
Async database session
|
|
61
|
+
"""
|
|
62
|
+
async_session = async_sessionmaker(
|
|
63
|
+
engine,
|
|
64
|
+
class_=AsyncSession,
|
|
65
|
+
expire_on_commit=False,
|
|
66
|
+
)
|
|
67
|
+
async with async_session() as session:
|
|
68
|
+
yield session
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI Dependencies
|
|
3
|
+
|
|
4
|
+
This module provides dependency injection utilities for FastAPI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import Request
|
|
8
|
+
{% if use_database %}
|
|
9
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
10
|
+
|
|
11
|
+
from app.core.database import DatabaseUtil
|
|
12
|
+
{% endif %}
|
|
13
|
+
|
|
14
|
+
{% if use_database %}
|
|
15
|
+
|
|
16
|
+
def get_db_engine(request: Request) -> AsyncEngine:
|
|
17
|
+
"""
|
|
18
|
+
Get database engine from request state.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
request: FastAPI request object
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Async database engine
|
|
25
|
+
"""
|
|
26
|
+
return request.app.state.db_engine
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def get_db_session(request: Request) -> AsyncSession:
|
|
30
|
+
"""
|
|
31
|
+
Get database session.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
request: FastAPI request object
|
|
35
|
+
|
|
36
|
+
Yields:
|
|
37
|
+
Async database session
|
|
38
|
+
"""
|
|
39
|
+
engine = get_db_engine(request)
|
|
40
|
+
async for session in DatabaseUtil.get_session(engine):
|
|
41
|
+
yield session
|
|
42
|
+
{% endif %}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_config(request: Request):
|
|
46
|
+
"""
|
|
47
|
+
Get application configuration from request state.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
request: FastAPI request object
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Application configuration
|
|
54
|
+
"""
|
|
55
|
+
return request.app.state.config
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Redis Utilities
|
|
3
|
+
|
|
4
|
+
This module provides Redis connection management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import redis.asyncio as redis
|
|
8
|
+
|
|
9
|
+
from app.config.env import RedisConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RedisUtil:
|
|
13
|
+
"""Redis utility class."""
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
async def init_redis(cls, config: RedisConfig) -> redis.Redis:
|
|
17
|
+
"""
|
|
18
|
+
Initialize Redis connection.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
config: Redis configuration
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Redis client instance
|
|
25
|
+
"""
|
|
26
|
+
client = redis.from_url(
|
|
27
|
+
config.redis_url,
|
|
28
|
+
encoding="utf-8",
|
|
29
|
+
decode_responses=True,
|
|
30
|
+
)
|
|
31
|
+
return client
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
async def close_redis(cls, client: redis.Redis) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Close Redis connection.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
client: Redis client to close
|
|
40
|
+
"""
|
|
41
|
+
await client.aclose()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Exceptions
|
|
3
|
+
|
|
4
|
+
This module defines custom exception classes for the application.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AppException(Exception):
|
|
9
|
+
"""Base application exception."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, message: str = "An error occurred"):
|
|
12
|
+
self.message = message
|
|
13
|
+
super().__init__(self.message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthException(AppException):
|
|
17
|
+
"""Authentication exception."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str = "Authentication failed"):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NotFoundException(AppException):
|
|
24
|
+
"""Resource not found exception."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, message: str = "Resource not found"):
|
|
27
|
+
super().__init__(message)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ValidationException(AppException):
|
|
31
|
+
"""Validation exception."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, message: str = "Validation error"):
|
|
34
|
+
super().__init__(message)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exception Handlers
|
|
3
|
+
|
|
4
|
+
This module registers global exception handlers for the FastAPI application.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import FastAPI, Request, status
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
from starlette.exceptions import HTTPException
|
|
10
|
+
|
|
11
|
+
from app.exceptions.exception import AppException, AuthException
|
|
12
|
+
from app.utils.log import log
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def register_exception_handlers(app: FastAPI) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Register global exception handlers.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
app: FastAPI application instance
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
@app.exception_handler(AuthException)
|
|
24
|
+
async def auth_exception_handler(request: Request, exc: AuthException):
|
|
25
|
+
"""Handle authentication exceptions."""
|
|
26
|
+
return JSONResponse(
|
|
27
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
28
|
+
content={"message": exc.message},
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
@app.exception_handler(AppException)
|
|
32
|
+
async def app_exception_handler(request: Request, exc: AppException):
|
|
33
|
+
"""Handle application exceptions."""
|
|
34
|
+
log.error(f"Application error: {exc.message}")
|
|
35
|
+
return JSONResponse(
|
|
36
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
37
|
+
content={"message": exc.message},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@app.exception_handler(HTTPException)
|
|
41
|
+
async def http_exception_handler(request: Request, exc: HTTPException):
|
|
42
|
+
"""Handle HTTP exceptions."""
|
|
43
|
+
log.error(f"HTTP error: {exc.detail}")
|
|
44
|
+
return JSONResponse(
|
|
45
|
+
status_code=exc.status_code,
|
|
46
|
+
content={"message": exc.detail},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@app.exception_handler(Exception)
|
|
50
|
+
async def general_exception_handler(request: Request, exc: Exception):
|
|
51
|
+
"""Handle all other exceptions."""
|
|
52
|
+
log.error(f"Unexpected error: {exc}")
|
|
53
|
+
return JSONResponse(
|
|
54
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
55
|
+
content={"message": "Internal server error"},
|
|
56
|
+
)
|