docs: add AI code generation requirements and comprehensive Google-style docstrings
- Add AI code generation requirements to AGENTS.md - Add module-level docstrings to all 46 Python modules - Add detailed Google-style docstrings to all classes and functions - Remove all inline comments following self-documenting code principle - Include Args, Returns, Raises sections in function docstrings - Add Attributes and Examples sections to class docstrings
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
"""API router configuration."""
|
||||
"""API router configuration.
|
||||
|
||||
This module sets up the main API router and includes versioned
|
||||
sub-routers for API organization.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""API dependencies using Dishka."""
|
||||
"""API dependencies using Dishka.
|
||||
|
||||
This module defines FastAPI dependencies for authentication, authorization,
|
||||
and use case injection using Dishka DI container.
|
||||
"""
|
||||
|
||||
from typing import Annotated, Any
|
||||
|
||||
@@ -18,7 +22,6 @@ from app.domain.exceptions import ForbiddenException, UnauthorizedException
|
||||
from app.domain.roles import Role, get_effective_role
|
||||
from app.infrastructure.auth import KeycloakAuthClient, TokenInfo
|
||||
|
||||
# Use case dependencies - injected via Dishka
|
||||
CreatePostDep = FromDishka[CreatePostUseCase]
|
||||
GetPostDep = FromDishka[GetPostUseCase]
|
||||
UpdatePostDep = FromDishka[UpdatePostUseCase]
|
||||
@@ -26,12 +29,18 @@ DeletePostDep = FromDishka[DeletePostUseCase]
|
||||
ListPostsDep = FromDishka[ListPostsUseCase]
|
||||
PublishPostDep = FromDishka[PublishPostUseCase]
|
||||
|
||||
# Security scheme
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
def get_keycloak_client(request: Request) -> KeycloakAuthClient:
|
||||
"""Get Keycloak client from DI container via request state."""
|
||||
"""Get Keycloak client from DI container via request state.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object.
|
||||
|
||||
Returns:
|
||||
KeycloakAuthClient instance from container.
|
||||
"""
|
||||
client: KeycloakAuthClient = request.state.dishka_container.get(KeycloakAuthClient)
|
||||
return client
|
||||
|
||||
@@ -40,7 +49,18 @@ async def get_current_token_info(
|
||||
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
|
||||
request: Request,
|
||||
) -> TokenInfo:
|
||||
"""Validate token and return token info from Keycloak."""
|
||||
"""Validate token and return token info from Keycloak.
|
||||
|
||||
Args:
|
||||
credentials: HTTP authorization credentials.
|
||||
request: FastAPI request object.
|
||||
|
||||
Returns:
|
||||
Validated TokenInfo instance.
|
||||
|
||||
Raises:
|
||||
UnauthorizedException: If no credentials or invalid token.
|
||||
"""
|
||||
if not credentials:
|
||||
raise UnauthorizedException("Authentication required")
|
||||
|
||||
@@ -57,7 +77,14 @@ async def get_current_token_info(
|
||||
async def get_current_user_id(
|
||||
token_info: Annotated[TokenInfo, Depends(get_current_token_info)],
|
||||
) -> str:
|
||||
"""Get current user ID from validated token."""
|
||||
"""Get current user ID from validated token.
|
||||
|
||||
Args:
|
||||
token_info: Validated token info.
|
||||
|
||||
Returns:
|
||||
User ID string from token.
|
||||
"""
|
||||
return token_info.user_id
|
||||
|
||||
|
||||
@@ -65,12 +92,21 @@ CurrentUserDep = Annotated[str, Depends(get_current_user_id)]
|
||||
TokenInfoDep = Annotated[TokenInfo, Depends(get_current_token_info)]
|
||||
|
||||
|
||||
# Optional auth - doesn't require authentication but provides user info if available
|
||||
async def get_optional_token_info(
|
||||
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
|
||||
request: Request,
|
||||
) -> TokenInfo | None:
|
||||
"""Get token info if valid token provided, otherwise None (guest)."""
|
||||
"""Get token info if valid token provided, otherwise None.
|
||||
|
||||
For endpoints that support both authenticated and guest access.
|
||||
|
||||
Args:
|
||||
credentials: HTTP authorization credentials.
|
||||
request: FastAPI request object.
|
||||
|
||||
Returns:
|
||||
TokenInfo if valid, None otherwise.
|
||||
"""
|
||||
if not credentials:
|
||||
return None
|
||||
|
||||
@@ -90,7 +126,14 @@ OptionalTokenInfoDep = Annotated[TokenInfo | None, Depends(get_optional_token_in
|
||||
async def get_optional_user_id(
|
||||
token_info: OptionalTokenInfoDep,
|
||||
) -> str | None:
|
||||
"""Get current user ID if token is valid, otherwise None."""
|
||||
"""Get current user ID if token is valid, otherwise None.
|
||||
|
||||
Args:
|
||||
token_info: Optional token info.
|
||||
|
||||
Returns:
|
||||
User ID if authenticated, None for guests.
|
||||
"""
|
||||
if token_info:
|
||||
return token_info.user_id
|
||||
return None
|
||||
@@ -103,6 +146,12 @@ def get_current_role(token_info: OptionalTokenInfoDep) -> Role:
|
||||
"""Get effective role from token info.
|
||||
|
||||
Returns GUEST if no valid token provided.
|
||||
|
||||
Args:
|
||||
token_info: Optional token info.
|
||||
|
||||
Returns:
|
||||
Effective Role enum value.
|
||||
"""
|
||||
if token_info and token_info.roles:
|
||||
return get_effective_role(token_info.roles)
|
||||
@@ -113,7 +162,17 @@ CurrentRoleDep = Annotated[Role, Depends(get_current_role)]
|
||||
|
||||
|
||||
def require_roles(allowed_roles: list[Role]) -> Any:
|
||||
"""Create dependency that checks if user has one of the allowed roles."""
|
||||
"""Create dependency that checks if user has one of the allowed roles.
|
||||
|
||||
Args:
|
||||
allowed_roles: List of roles allowed to access.
|
||||
|
||||
Returns:
|
||||
FastAPI Depends for role checking.
|
||||
|
||||
Raises:
|
||||
ForbiddenException: If user role is not in allowed list.
|
||||
"""
|
||||
|
||||
async def check_role(role: CurrentRoleDep) -> Role:
|
||||
if role not in allowed_roles:
|
||||
@@ -125,7 +184,6 @@ def require_roles(allowed_roles: list[Role]) -> Any:
|
||||
return Depends(check_role)
|
||||
|
||||
|
||||
# Predefined role requirements
|
||||
RequireAdmin = require_roles([Role.ADMIN])
|
||||
RequireUser = require_roles([Role.USER, Role.ADMIN])
|
||||
RequireAny = require_roles([Role.GUEST, Role.USER, Role.ADMIN])
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""API v1 router."""
|
||||
"""API v1 router.
|
||||
|
||||
This module sets up the version 1 API router and includes
|
||||
all v1 endpoint routers.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Posts API routes."""
|
||||
"""Posts API routes.
|
||||
|
||||
This module defines FastAPI routes for blog post operations.
|
||||
Implements CRUD endpoints with authentication and authorization.
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
@@ -39,7 +43,16 @@ async def create_post(
|
||||
use_case: CreatePostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Create a new blog post."""
|
||||
"""Create a new blog post.
|
||||
|
||||
Args:
|
||||
schema: Post creation data.
|
||||
use_case: CreatePostUseCase dependency.
|
||||
current_user_id: Authenticated user ID.
|
||||
|
||||
Returns:
|
||||
PostResponseSchema with created post data.
|
||||
"""
|
||||
dto = CreatePostDTO(
|
||||
title=schema.title,
|
||||
content=schema.content,
|
||||
@@ -65,19 +78,22 @@ async def list_posts(
|
||||
"""Get blog posts with optional filtering and pagination.
|
||||
|
||||
Args:
|
||||
use_case: ListPostsUseCase dependency.
|
||||
role: Current user role.
|
||||
include_unpublished: If True, returns all posts including drafts.
|
||||
Only admins can use this parameter.
|
||||
Only admins can use this parameter.
|
||||
limit: Maximum number of posts to return (default: 10, max: 100).
|
||||
offset: Number of posts to skip (default: 0).
|
||||
|
||||
Returns:
|
||||
PostListResponseSchema with paginated posts.
|
||||
|
||||
Raises:
|
||||
ForbiddenException: If non-admin tries to include unpublished posts.
|
||||
"""
|
||||
# Clamp limit to reasonable range
|
||||
limit = max(1, min(limit, 100))
|
||||
offset = max(0, offset)
|
||||
|
||||
# Check permissions for unpublished posts
|
||||
if include_unpublished:
|
||||
if not has_permission(role, Permission.POST_READ_UNPUBLISHED):
|
||||
raise ForbiddenException("Only admins can view unpublished posts")
|
||||
@@ -97,7 +113,14 @@ async def list_posts(
|
||||
async def list_published_posts(
|
||||
use_case: ListPostsDep,
|
||||
) -> PostListResponseSchema:
|
||||
"""Get all published blog posts."""
|
||||
"""Get all published blog posts.
|
||||
|
||||
Args:
|
||||
use_case: ListPostsUseCase dependency.
|
||||
|
||||
Returns:
|
||||
PostListResponseSchema with published posts.
|
||||
"""
|
||||
results = await use_case.published_posts()
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
@@ -112,7 +135,15 @@ async def search_posts(
|
||||
query: str,
|
||||
use_case: ListPostsDep,
|
||||
) -> PostListResponseSchema:
|
||||
"""Search posts by query."""
|
||||
"""Search posts by query.
|
||||
|
||||
Args:
|
||||
query: Search query string.
|
||||
use_case: ListPostsUseCase dependency.
|
||||
|
||||
Returns:
|
||||
PostListResponseSchema with matching posts.
|
||||
"""
|
||||
results = await use_case.search(query)
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
@@ -127,7 +158,15 @@ async def get_posts_by_tag(
|
||||
tag: str,
|
||||
use_case: ListPostsDep,
|
||||
) -> PostListResponseSchema:
|
||||
"""Get posts by tag."""
|
||||
"""Get posts by tag.
|
||||
|
||||
Args:
|
||||
tag: Tag to filter by.
|
||||
use_case: ListPostsUseCase dependency.
|
||||
|
||||
Returns:
|
||||
PostListResponseSchema with tagged posts.
|
||||
"""
|
||||
results = await use_case.by_tag(tag)
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
@@ -142,7 +181,15 @@ async def get_posts_by_author(
|
||||
author_id: str,
|
||||
use_case: ListPostsDep,
|
||||
) -> PostListResponseSchema:
|
||||
"""Get posts by author."""
|
||||
"""Get posts by author.
|
||||
|
||||
Args:
|
||||
author_id: Author identifier.
|
||||
use_case: ListPostsUseCase dependency.
|
||||
|
||||
Returns:
|
||||
PostListResponseSchema with author's posts.
|
||||
"""
|
||||
results = await use_case.by_author(author_id)
|
||||
items = [PostResponseSchema(**r.__dict__) for r in results]
|
||||
return PostListResponseSchema(items=items, total=len(items))
|
||||
@@ -157,7 +204,15 @@ async def get_post(
|
||||
post_id: UUID,
|
||||
use_case: GetPostDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Get a post by its ID."""
|
||||
"""Get a post by its ID.
|
||||
|
||||
Args:
|
||||
post_id: Unique post identifier.
|
||||
use_case: GetPostUseCase dependency.
|
||||
|
||||
Returns:
|
||||
PostResponseSchema with post data.
|
||||
"""
|
||||
result = await use_case.by_id(post_id)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
@@ -171,7 +226,15 @@ async def get_post_by_slug(
|
||||
slug: str,
|
||||
use_case: GetPostDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Get a post by its slug."""
|
||||
"""Get a post by its slug.
|
||||
|
||||
Args:
|
||||
slug: URL-friendly slug identifier.
|
||||
use_case: GetPostUseCase dependency.
|
||||
|
||||
Returns:
|
||||
PostResponseSchema with post data.
|
||||
"""
|
||||
result = await use_case.by_slug(slug)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
@@ -187,7 +250,17 @@ async def update_post(
|
||||
use_case: UpdatePostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Update a post."""
|
||||
"""Update a post.
|
||||
|
||||
Args:
|
||||
post_id: Unique post identifier.
|
||||
schema: Update data.
|
||||
use_case: UpdatePostUseCase dependency.
|
||||
current_user_id: Authenticated user ID.
|
||||
|
||||
Returns:
|
||||
PostResponseSchema with updated post data.
|
||||
"""
|
||||
dto = UpdatePostDTO(
|
||||
title=schema.title,
|
||||
content=schema.content,
|
||||
@@ -207,7 +280,13 @@ async def delete_post(
|
||||
use_case: DeletePostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> None:
|
||||
"""Delete a post."""
|
||||
"""Delete a post.
|
||||
|
||||
Args:
|
||||
post_id: Unique post identifier.
|
||||
use_case: DeletePostUseCase dependency.
|
||||
current_user_id: Authenticated user ID.
|
||||
"""
|
||||
await use_case.execute(post_id, current_user_id)
|
||||
|
||||
|
||||
@@ -221,7 +300,16 @@ async def publish_post(
|
||||
use_case: PublishPostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Publish a post."""
|
||||
"""Publish a post.
|
||||
|
||||
Args:
|
||||
post_id: Unique post identifier.
|
||||
use_case: PublishPostUseCase dependency.
|
||||
current_user_id: Authenticated user ID.
|
||||
|
||||
Returns:
|
||||
PostResponseSchema with published post data.
|
||||
"""
|
||||
result = await use_case.publish(post_id, current_user_id)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
@@ -236,6 +324,15 @@ async def unpublish_post(
|
||||
use_case: PublishPostDep,
|
||||
current_user_id: CurrentUserDep,
|
||||
) -> PostResponseSchema:
|
||||
"""Unpublish a post."""
|
||||
"""Unpublish a post.
|
||||
|
||||
Args:
|
||||
post_id: Unique post identifier.
|
||||
use_case: PublishPostUseCase dependency.
|
||||
current_user_id: Authenticated user ID.
|
||||
|
||||
Returns:
|
||||
PostResponseSchema with unpublished post data.
|
||||
"""
|
||||
result = await use_case.unpublish(post_id, current_user_id)
|
||||
return PostResponseSchema(**result.__dict__)
|
||||
|
||||
Reference in New Issue
Block a user