chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
# FastAPI Endpoints Generator
|
||||
|
||||
Create comprehensive FastAPI endpoints with proper structure, validation, and documentation.
|
||||
|
||||
## Purpose
|
||||
|
||||
This command helps you quickly create FastAPI endpoints with Pydantic models, dependency injection, and automatic API documentation.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/api-endpoints
|
||||
```
|
||||
|
||||
## What this command does
|
||||
|
||||
1. **Creates API endpoints** with proper HTTP methods
|
||||
2. **Adds Pydantic models** for request/response validation
|
||||
3. **Implements dependency injection** for database and auth
|
||||
4. **Includes error handling** and status codes
|
||||
5. **Generates automatic documentation** with OpenAPI
|
||||
|
||||
## Example Output
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from fastapi import FastAPI, Depends, HTTPException, status
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
import uvicorn
|
||||
|
||||
from app.database import get_db, engine
|
||||
from app.models import models
|
||||
from app.routers import auth, users, posts, comments
|
||||
from app.core.config import settings
|
||||
|
||||
# Create database tables
|
||||
models.Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Blog API",
|
||||
description="A comprehensive blog API built with FastAPI",
|
||||
version="1.0.0",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc"
|
||||
)
|
||||
|
||||
# Add CORS middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.ALLOWED_HOSTS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
app.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
app.include_router(posts.router, prefix="/posts", tags=["Posts"])
|
||||
app.include_router(comments.router, prefix="/comments", tags=["Comments"])
|
||||
|
||||
@app.get("/", tags=["Root"])
|
||||
async def root():
|
||||
"""API root endpoint."""
|
||||
return {
|
||||
"message": "Welcome to Blog API",
|
||||
"version": "1.0.0",
|
||||
"docs": "/docs",
|
||||
"redoc": "/redoc"
|
||||
}
|
||||
|
||||
@app.get("/health", tags=["Health"])
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {"status": "healthy", "timestamp": "2024-01-01T00:00:00Z"}
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
# app/routers/posts.py
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.database import get_db
|
||||
from app.schemas import post_schemas
|
||||
from app.services import post_service
|
||||
from app.core.dependencies import get_current_user, get_current_active_user
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[post_schemas.PostResponse])
|
||||
async def get_posts(
|
||||
skip: int = Query(0, ge=0, description="Number of posts to skip"),
|
||||
limit: int = Query(10, ge=1, le=100, description="Number of posts to return"),
|
||||
search: Optional[str] = Query(None, description="Search in title and content"),
|
||||
category: Optional[str] = Query(None, description="Filter by category"),
|
||||
published: Optional[bool] = Query(True, description="Filter by published status"),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get all posts with pagination and filtering.
|
||||
|
||||
- **skip**: Number of posts to skip (for pagination)
|
||||
- **limit**: Maximum number of posts to return (1-100)
|
||||
- **search**: Search term for title and content
|
||||
- **category**: Filter posts by category
|
||||
- **published**: Filter by published status
|
||||
"""
|
||||
posts = post_service.get_posts(
|
||||
db=db,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
search=search,
|
||||
category=category,
|
||||
published=published
|
||||
)
|
||||
return posts
|
||||
|
||||
@router.get("/{post_id}", response_model=post_schemas.PostResponse)
|
||||
async def get_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get a specific post by ID.
|
||||
|
||||
- **post_id**: Unique identifier for the post
|
||||
"""
|
||||
post = post_service.get_post(db=db, post_id=post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
return post
|
||||
|
||||
@router.post("/", response_model=post_schemas.PostResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_post(
|
||||
post: post_schemas.PostCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Create a new post.
|
||||
|
||||
- **title**: Post title (required)
|
||||
- **content**: Post content (required)
|
||||
- **category**: Post category (optional)
|
||||
- **published**: Publication status (default: false)
|
||||
"""
|
||||
return post_service.create_post(
|
||||
db=db,
|
||||
post=post,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
@router.put("/{post_id}", response_model=post_schemas.PostResponse)
|
||||
async def update_post(
|
||||
post_id: int,
|
||||
post_update: post_schemas.PostUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Update an existing post.
|
||||
|
||||
- **post_id**: Unique identifier for the post
|
||||
- **title**: Updated post title (optional)
|
||||
- **content**: Updated post content (optional)
|
||||
- **category**: Updated post category (optional)
|
||||
- **published**: Updated publication status (optional)
|
||||
"""
|
||||
post = post_service.get_post(db=db, post_id=post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
if post.author_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to update this post"
|
||||
)
|
||||
|
||||
return post_service.update_post(
|
||||
db=db,
|
||||
post_id=post_id,
|
||||
post_update=post_update
|
||||
)
|
||||
|
||||
@router.delete("/{post_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Delete a post.
|
||||
|
||||
- **post_id**: Unique identifier for the post to delete
|
||||
"""
|
||||
post = post_service.get_post(db=db, post_id=post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
if post.author_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Not authorized to delete this post"
|
||||
)
|
||||
|
||||
post_service.delete_post(db=db, post_id=post_id)
|
||||
|
||||
@router.post("/{post_id}/like", response_model=post_schemas.PostResponse)
|
||||
async def like_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""
|
||||
Like/unlike a post.
|
||||
|
||||
- **post_id**: Unique identifier for the post to like
|
||||
"""
|
||||
post = post_service.get_post(db=db, post_id=post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
return post_service.toggle_like(
|
||||
db=db,
|
||||
post_id=post_id,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
@router.get("/{post_id}/comments", response_model=List[post_schemas.CommentResponse])
|
||||
async def get_post_comments(
|
||||
post_id: int,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(10, ge=1, le=100),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get all comments for a specific post.
|
||||
|
||||
- **post_id**: Unique identifier for the post
|
||||
- **skip**: Number of comments to skip
|
||||
- **limit**: Maximum number of comments to return
|
||||
"""
|
||||
post = post_service.get_post(db=db, post_id=post_id)
|
||||
if not post:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Post not found"
|
||||
)
|
||||
|
||||
return post_service.get_post_comments(
|
||||
db=db,
|
||||
post_id=post_id,
|
||||
skip=skip,
|
||||
limit=limit
|
||||
)
|
||||
```
|
||||
|
||||
```python
|
||||
# app/schemas/post_schemas.py
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
class PostBase(BaseModel):
|
||||
"""Base post schema."""
|
||||
title: str = Field(..., min_length=1, max_length=200, description="Post title")
|
||||
content: str = Field(..., min_length=1, description="Post content")
|
||||
category: Optional[str] = Field(None, max_length=50, description="Post category")
|
||||
published: bool = Field(False, description="Publication status")
|
||||
|
||||
class PostCreate(PostBase):
|
||||
"""Schema for creating a post."""
|
||||
|
||||
@validator('title')
|
||||
def validate_title(cls, v):
|
||||
if not v.strip():
|
||||
raise ValueError('Title cannot be empty')
|
||||
return v.strip()
|
||||
|
||||
@validator('content')
|
||||
def validate_content(cls, v):
|
||||
if len(v.strip()) < 10:
|
||||
raise ValueError('Content must be at least 10 characters long')
|
||||
return v.strip()
|
||||
|
||||
class PostUpdate(BaseModel):
|
||||
"""Schema for updating a post."""
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=200)
|
||||
content: Optional[str] = Field(None, min_length=1)
|
||||
category: Optional[str] = Field(None, max_length=50)
|
||||
published: Optional[bool] = None
|
||||
|
||||
@validator('title')
|
||||
def validate_title(cls, v):
|
||||
if v is not None and not v.strip():
|
||||
raise ValueError('Title cannot be empty')
|
||||
return v.strip() if v else v
|
||||
|
||||
@validator('content')
|
||||
def validate_content(cls, v):
|
||||
if v is not None and len(v.strip()) < 10:
|
||||
raise ValueError('Content must be at least 10 characters long')
|
||||
return v.strip() if v else v
|
||||
|
||||
class PostResponse(PostBase):
|
||||
"""Schema for post responses."""
|
||||
id: int
|
||||
author_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
like_count: int = 0
|
||||
comment_count: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class CommentBase(BaseModel):
|
||||
"""Base comment schema."""
|
||||
content: str = Field(..., min_length=1, max_length=1000, description="Comment content")
|
||||
|
||||
class CommentCreate(CommentBase):
|
||||
"""Schema for creating a comment."""
|
||||
post_id: int = Field(..., description="ID of the post to comment on")
|
||||
|
||||
class CommentResponse(CommentBase):
|
||||
"""Schema for comment responses."""
|
||||
id: int
|
||||
post_id: int
|
||||
author_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
```python
|
||||
# app/services/post_service.py
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_
|
||||
|
||||
from app.models.post import Post, Comment, PostLike
|
||||
from app.schemas.post_schemas import PostCreate, PostUpdate
|
||||
|
||||
def get_posts(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 10,
|
||||
search: Optional[str] = None,
|
||||
category: Optional[str] = None,
|
||||
published: Optional[bool] = True
|
||||
) -> List[Post]:
|
||||
"""Get posts with filtering and pagination."""
|
||||
query = db.query(Post)
|
||||
|
||||
if published is not None:
|
||||
query = query.filter(Post.published == published)
|
||||
|
||||
if category:
|
||||
query = query.filter(Post.category == category)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Post.title.contains(search),
|
||||
Post.content.contains(search)
|
||||
)
|
||||
)
|
||||
|
||||
return query.offset(skip).limit(limit).all()
|
||||
|
||||
def get_post(db: Session, post_id: int) -> Optional[Post]:
|
||||
"""Get a single post by ID."""
|
||||
return db.query(Post).filter(Post.id == post_id).first()
|
||||
|
||||
def create_post(db: Session, post: PostCreate, user_id: int) -> Post:
|
||||
"""Create a new post."""
|
||||
db_post = Post(**post.dict(), author_id=user_id)
|
||||
db.add(db_post)
|
||||
db.commit()
|
||||
db.refresh(db_post)
|
||||
return db_post
|
||||
|
||||
def update_post(
|
||||
db: Session,
|
||||
post_id: int,
|
||||
post_update: PostUpdate
|
||||
) -> Optional[Post]:
|
||||
"""Update an existing post."""
|
||||
db_post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not db_post:
|
||||
return None
|
||||
|
||||
update_data = post_update.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_post, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_post)
|
||||
return db_post
|
||||
|
||||
def delete_post(db: Session, post_id: int) -> bool:
|
||||
"""Delete a post."""
|
||||
db_post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not db_post:
|
||||
return False
|
||||
|
||||
db.delete(db_post)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
def toggle_like(db: Session, post_id: int, user_id: int) -> Post:
|
||||
"""Toggle like status for a post."""
|
||||
existing_like = db.query(PostLike).filter(
|
||||
PostLike.post_id == post_id,
|
||||
PostLike.user_id == user_id
|
||||
).first()
|
||||
|
||||
if existing_like:
|
||||
db.delete(existing_like)
|
||||
else:
|
||||
new_like = PostLike(post_id=post_id, user_id=user_id)
|
||||
db.add(new_like)
|
||||
|
||||
db.commit()
|
||||
return get_post(db, post_id)
|
||||
```
|
||||
|
||||
## Features Included
|
||||
|
||||
### API Documentation
|
||||
- **Automatic OpenAPI** schema generation
|
||||
- **Interactive docs** at `/docs`
|
||||
- **ReDoc documentation** at `/redoc`
|
||||
- **Request/Response examples** in schemas
|
||||
|
||||
### Validation & Serialization
|
||||
- **Pydantic models** for data validation
|
||||
- **Custom validators** for business rules
|
||||
- **Type hints** for better IDE support
|
||||
- **Automatic data conversion** and validation
|
||||
|
||||
### Error Handling
|
||||
- **HTTP status codes** for different scenarios
|
||||
- **Detailed error messages** with context
|
||||
- **Input validation errors** with field-specific messages
|
||||
- **Custom exception handlers** for consistent responses
|
||||
|
||||
### Security
|
||||
- **JWT authentication** with dependencies
|
||||
- **Role-based access control** for endpoints
|
||||
- **CORS middleware** for cross-origin requests
|
||||
- **Input sanitization** through Pydantic
|
||||
|
||||
### Performance
|
||||
- **Database query optimization** with SQLAlchemy
|
||||
- **Pagination support** for large datasets
|
||||
- **Async/await support** for concurrent requests
|
||||
- **Connection pooling** for database efficiency
|
||||
|
||||
## Testing Example
|
||||
|
||||
```python
|
||||
# tests/test_posts.py
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def test_get_posts():
|
||||
"""Test getting posts."""
|
||||
response = client.get("/posts/")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
def test_create_post():
|
||||
"""Test creating a new post."""
|
||||
post_data = {
|
||||
"title": "Test Post",
|
||||
"content": "This is a test post content.",
|
||||
"published": True
|
||||
}
|
||||
response = client.post("/posts/", json=post_data)
|
||||
assert response.status_code == 201
|
||||
assert response.json()["title"] == "Test Post"
|
||||
```
|
||||
Reference in New Issue
Block a user