refactor: remove all inline comments from code
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
"""Common error response schema and exception handlers."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
@@ -11,8 +9,6 @@ from app.core.exceptions import AppException
|
||||
|
||||
|
||||
class ErrorResponse(BaseModel):
|
||||
"""Standard error response format."""
|
||||
|
||||
status_code: int
|
||||
message: str
|
||||
details: dict | None = None
|
||||
@@ -20,7 +16,6 @@ class ErrorResponse(BaseModel):
|
||||
|
||||
|
||||
async def app_exception_handler(request: Request, exc: AppException) -> JSONResponse:
|
||||
"""Handle application exceptions with standard response."""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
@@ -32,7 +27,6 @@ async def app_exception_handler(request: Request, exc: AppException) -> JSONResp
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
"""Handle HTTP exceptions with standard response."""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
@@ -44,7 +38,6 @@ async def http_exception_handler(request: Request, exc: HTTPException) -> JSONRe
|
||||
|
||||
|
||||
def register_exception_handlers(app: FastAPI):
|
||||
"""Register all exception handlers with FastAPI app."""
|
||||
app.add_exception_handler(
|
||||
AppException,
|
||||
app_exception_handler, # type: ignore[arg-type]
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
"""Application configuration and settings."""
|
||||
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings from environment variables."""
|
||||
|
||||
app_name: str = "Blog API"
|
||||
debug: bool = False
|
||||
host: str = "0.0.0.0"
|
||||
port: int = 8000
|
||||
|
||||
# Database (when added)
|
||||
database_url: str | None = None
|
||||
|
||||
model_config = SettingsConfigDict(env_file=".env")
|
||||
|
||||
@@ -1,64 +1,25 @@
|
||||
"""Custom application exceptions."""
|
||||
|
||||
|
||||
class AppException(Exception):
|
||||
"""Base application exception."""
|
||||
|
||||
def __init__(self, message: str, status_code: int = 500):
|
||||
"""Initialize application exception.
|
||||
|
||||
Args:
|
||||
message: Error message.
|
||||
status_code: HTTP status code.
|
||||
"""
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
super().__init__(self.message)
|
||||
|
||||
|
||||
class NotFoundError(AppException):
|
||||
"""Resource not found error."""
|
||||
|
||||
def __init__(self, message: str = "Resource not found"):
|
||||
"""Initialize not found error.
|
||||
|
||||
Args:
|
||||
message: Error message.
|
||||
"""
|
||||
super().__init__(message, status_code=404)
|
||||
|
||||
|
||||
class ValidationError(AppException):
|
||||
"""Validation error."""
|
||||
|
||||
def __init__(self, message: str = "Validation failed"):
|
||||
"""Initialize validation error.
|
||||
|
||||
Args:
|
||||
message: Error message.
|
||||
"""
|
||||
super().__init__(message, status_code=400)
|
||||
|
||||
|
||||
class UnauthorizedError(AppException):
|
||||
"""Authentication required."""
|
||||
|
||||
def __init__(self, message: str = "Unauthorized"):
|
||||
"""Initialize unauthorized error.
|
||||
|
||||
Args:
|
||||
message: Error message.
|
||||
"""
|
||||
super().__init__(message, status_code=401)
|
||||
|
||||
|
||||
class ForbiddenError(AppException):
|
||||
"""Permission denied."""
|
||||
|
||||
def __init__(self, message: str = "Forbidden"):
|
||||
"""Initialize forbidden error.
|
||||
|
||||
Args:
|
||||
message: Error message.
|
||||
"""
|
||||
super().__init__(message, status_code=403)
|
||||
|
||||
16
app/main.py
16
app/main.py
@@ -1,5 +1,3 @@
|
||||
"""FastAPI application factory and entry point."""
|
||||
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import uvicorn
|
||||
@@ -11,32 +9,18 @@ from app.core.config import settings
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager for startup/shutdown events."""
|
||||
# Startup: initialize DB connections, cache, etc.
|
||||
yield
|
||||
# Shutdown: cleanup resources
|
||||
|
||||
|
||||
def app_factory() -> FastAPI:
|
||||
"""Create and configure FastAPI application instance.
|
||||
|
||||
Returns:
|
||||
Configured FastAPI application.
|
||||
"""
|
||||
app = FastAPI(title=settings.app_name, debug=settings.debug, lifespan=lifespan)
|
||||
|
||||
# Register exception handlers
|
||||
register_exception_handlers(app)
|
||||
|
||||
# Register routers (when added)
|
||||
# from app.api.v1.router import api_router
|
||||
# app.include_router(api_router, prefix="/api/v1")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def main():
|
||||
"""Run the application with uvicorn server."""
|
||||
uvicorn.run(app_factory, factory=True, host=settings.host, port=settings.port)
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Clean all Python cache files
|
||||
|
||||
set -e
|
||||
|
||||
echo "Cleaning Python cache files..."
|
||||
|
||||
# Find and remove __pycache__ directories
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
# Find and remove .pyc files
|
||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
|
||||
# Find and remove .pyo files
|
||||
find . -type f -name "*.pyo" -delete 2>/dev/null || true
|
||||
|
||||
# Clean pytest cache
|
||||
rm -rf .pytest_cache/ 2>/dev/null || true
|
||||
|
||||
# Clean mypy cache
|
||||
rm -rf .mypy_cache/ 2>/dev/null || true
|
||||
|
||||
# Clean ruff cache
|
||||
rm -rf .ruff_cache/ 2>/dev/null || true
|
||||
|
||||
# Clean coverage
|
||||
rm -f .coverage 2>/dev/null || true
|
||||
rm -rf htmlcov/ 2>/dev/null || true
|
||||
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
#!/bin/bash
|
||||
# Pre-commit hook: Validate commit message and check for cache files
|
||||
|
||||
set -e
|
||||
|
||||
# Get commit message file
|
||||
COMMIT_MSG_FILE="$1"
|
||||
if [ -z "$COMMIT_MSG_FILE" ]; then
|
||||
# If called without args, check staged changes for cache files
|
||||
echo "Checking for cache files in staged changes..."
|
||||
|
||||
CACHE_FILES=$(git diff --cached --name-only | grep -E "__pycache__|\.pyc$|\.pyo$" || true)
|
||||
@@ -26,10 +23,8 @@ if [ -z "$COMMIT_MSG_FILE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Validate commit message format
|
||||
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
||||
|
||||
# Pattern: type: lowercase description (max 50 chars, no period)
|
||||
if ! echo "$COMMIT_MSG" | grep -qE "^(feat|fix|docs|style|refactor|test|chore): [a-z].{0,49}$"; then
|
||||
echo "❌ Invalid commit message format!"
|
||||
echo ""
|
||||
@@ -60,7 +55,6 @@ if ! echo "$COMMIT_MSG" | grep -qE "^(feat|fix|docs|style|refactor|test|chore):
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for period at end
|
||||
if echo "$COMMIT_MSG" | grep -qE "\.$"; then
|
||||
echo "❌ Commit message should not end with a period"
|
||||
exit 1
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Update README.md with latest project information.
|
||||
|
||||
This script:
|
||||
- Updates Changelog from git commits
|
||||
- Updates Dependencies from pyproject.toml
|
||||
- Updates Commands from available scripts
|
||||
- Updates CI/CD badges
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
@@ -17,19 +9,16 @@ from typing import Any
|
||||
|
||||
|
||||
def get_project_root() -> Path:
|
||||
"""Get project root directory."""
|
||||
return Path(__file__).parent.parent
|
||||
|
||||
|
||||
def get_pyproject() -> dict[str, Any]:
|
||||
"""Parse pyproject.toml."""
|
||||
root = get_project_root()
|
||||
with open(root / "pyproject.toml", "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
|
||||
def get_latest_commits(count: int = 10) -> list[dict[str, str]]:
|
||||
"""Get latest commits with hash, message, date."""
|
||||
result = subprocess.run(
|
||||
["git", "log", "--format=%H|%s|%ad|%an", "--date=short", f"-n{count}"],
|
||||
capture_output=True,
|
||||
@@ -54,7 +43,6 @@ def get_latest_commits(count: int = 10) -> list[dict[str, str]]:
|
||||
|
||||
|
||||
def get_last_tag() -> str | None:
|
||||
"""Get last git tag."""
|
||||
result = subprocess.run(
|
||||
["git", "describe", "--tags", "--abbrev=0"],
|
||||
capture_output=True,
|
||||
@@ -65,20 +53,17 @@ def get_last_tag() -> str | None:
|
||||
|
||||
|
||||
def get_ignored_files() -> set[str]:
|
||||
"""Get list of ignored file patterns from .gitignore."""
|
||||
gitignore_path = get_project_root() / ".gitignore"
|
||||
ignored = set()
|
||||
if gitignore_path.exists():
|
||||
for line in gitignore_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#"):
|
||||
# Remove trailing slash for directories
|
||||
ignored.add(line.rstrip("/"))
|
||||
return ignored
|
||||
|
||||
|
||||
def commit_has_tracked_changes(commit_hash: str) -> bool:
|
||||
"""Check if commit has changes to tracked (non-ignored) files."""
|
||||
result = subprocess.run(
|
||||
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", commit_hash],
|
||||
capture_output=True,
|
||||
@@ -92,7 +77,6 @@ def commit_has_tracked_changes(commit_hash: str) -> bool:
|
||||
for file_path in result.stdout.strip().split("\n"):
|
||||
if not file_path:
|
||||
continue
|
||||
# Check if file or its parent dir is ignored
|
||||
parts = file_path.split("/")
|
||||
is_ignored = False
|
||||
for i in range(len(parts)):
|
||||
@@ -113,7 +97,6 @@ def commit_has_tracked_changes(commit_hash: str) -> bool:
|
||||
|
||||
|
||||
def commit_has_skip_ci_message(commit_hash: str) -> bool:
|
||||
"""Check if commit message contains [skip ci] or similar."""
|
||||
result = subprocess.run(
|
||||
["git", "log", "-1", "--format=%s", commit_hash],
|
||||
capture_output=True,
|
||||
@@ -125,7 +108,6 @@ def commit_has_skip_ci_message(commit_hash: str) -> bool:
|
||||
|
||||
|
||||
def commit_only_changes_readme(commit_hash: str) -> bool:
|
||||
"""Check if commit only changes README.md."""
|
||||
result = subprocess.run(
|
||||
["git", "diff-tree", "--no-commit-id", "--name-only", "-r", commit_hash],
|
||||
capture_output=True,
|
||||
@@ -137,7 +119,6 @@ def commit_only_changes_readme(commit_hash: str) -> bool:
|
||||
|
||||
|
||||
def get_commits_since_tag(tag: str | None) -> list[dict[str, str]]:
|
||||
"""Get commits since last tag (only commits with tracked file changes)."""
|
||||
if tag:
|
||||
result = subprocess.run(
|
||||
["git", "log", "--format=%H|%s|%ad|%an", "--date=short", f"{tag}..HEAD"],
|
||||
@@ -159,13 +140,10 @@ def get_commits_since_tag(tag: str | None) -> list[dict[str, str]]:
|
||||
parts = line.split("|")
|
||||
if len(parts) >= 4:
|
||||
commit_hash = parts[0]
|
||||
# Skip commits with [skip ci] in message
|
||||
if commit_has_skip_ci_message(commit_hash):
|
||||
continue
|
||||
# Skip commits that only change README.md
|
||||
if commit_only_changes_readme(commit_hash):
|
||||
continue
|
||||
# Skip commits that only change ignored files
|
||||
if not commit_has_tracked_changes(commit_hash):
|
||||
continue
|
||||
commits.append(
|
||||
@@ -180,7 +158,6 @@ def get_commits_since_tag(tag: str | None) -> list[dict[str, str]]:
|
||||
|
||||
|
||||
def categorize_commits(commits: list[dict[str, str]]) -> dict[str, list[str]]:
|
||||
"""Categorize commits by type (feat, fix, docs, etc.)."""
|
||||
categories: dict[str, list[str]] = {
|
||||
"Added": [],
|
||||
"Changed": [],
|
||||
@@ -208,7 +185,6 @@ def categorize_commits(commits: list[dict[str, str]]) -> dict[str, list[str]]:
|
||||
|
||||
|
||||
def format_changelog(commits: list[dict[str, str]], version: str = "v0.1.0") -> str:
|
||||
"""Format commits as GitHub Releases changelog."""
|
||||
categorized = categorize_commits(commits)
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
@@ -223,7 +199,6 @@ def format_changelog(commits: list[dict[str, str]], version: str = "v0.1.0") ->
|
||||
|
||||
|
||||
def get_dependencies(pyproject: dict[str, Any]) -> dict[str, list[str]]:
|
||||
"""Extract dependencies from pyproject.toml."""
|
||||
deps: dict[str, list[str]] = {
|
||||
"runtime": [],
|
||||
"tests": [],
|
||||
@@ -232,11 +207,9 @@ def get_dependencies(pyproject: dict[str, Any]) -> dict[str, list[str]]:
|
||||
"docs": [],
|
||||
}
|
||||
|
||||
# Runtime dependencies
|
||||
for dep in pyproject.get("project", {}).get("dependencies", []):
|
||||
deps["runtime"].append(dep)
|
||||
|
||||
# Dev dependency groups
|
||||
dep_groups = pyproject.get("dependency-groups", {})
|
||||
|
||||
if "tests" in dep_groups:
|
||||
@@ -263,7 +236,6 @@ def get_dependencies(pyproject: dict[str, Any]) -> dict[str, list[str]]:
|
||||
|
||||
|
||||
def get_available_commands() -> list[dict[str, str]]:
|
||||
"""Get available uv commands from pyproject.toml."""
|
||||
commands = [
|
||||
{"cmd": "uv sync", "desc": "Install dependencies"},
|
||||
{"cmd": "uv run python -m app.main", "desc": "Start development server"},
|
||||
@@ -285,7 +257,6 @@ def get_available_commands() -> list[dict[str, str]]:
|
||||
|
||||
|
||||
def update_dependencies_section(content: str, deps: dict[str, list[str]]) -> str:
|
||||
"""Update Dependencies section in README."""
|
||||
section_pattern = r"(## Dependencies\n.*?)(\n## |\Z)"
|
||||
|
||||
deps_text = "## Dependencies\n\n"
|
||||
@@ -313,7 +284,6 @@ def update_dependencies_section(content: str, deps: dict[str, list[str]]) -> str
|
||||
|
||||
|
||||
def update_commands_section(content: str, commands: list[dict[str, str]]) -> str:
|
||||
"""Update Commands section in README."""
|
||||
section_pattern = r"(## Available Commands\n.*?\|.*?\n\|---\|.*?\n)(.*?)(\n## |\Z)"
|
||||
|
||||
commands_table = "| Command | Description |\n|---------|-------------|\n"
|
||||
@@ -327,7 +297,6 @@ def update_commands_section(content: str, commands: list[dict[str, str]]) -> str
|
||||
|
||||
|
||||
def update_changelog_section(content: str, changelog: str) -> str:
|
||||
"""Update Changelog section in README."""
|
||||
section_pattern = r"(## Changelog\n)(.*?)(\Z)"
|
||||
|
||||
replacement = f"\\1\n{changelog}\n\\3"
|
||||
@@ -335,14 +304,6 @@ def update_changelog_section(content: str, changelog: str) -> str:
|
||||
|
||||
|
||||
def update_readme(check_only: bool = False) -> bool:
|
||||
"""Update README.md with latest information.
|
||||
|
||||
Args:
|
||||
check_only: If True, only check if update needed (for CI)
|
||||
|
||||
Returns:
|
||||
True if changes were made (or needed in check mode)
|
||||
"""
|
||||
readme_path = get_project_root() / "README.md"
|
||||
|
||||
if not readme_path.exists():
|
||||
@@ -352,17 +313,14 @@ def update_readme(check_only: bool = False) -> bool:
|
||||
content = readme_path.read_text()
|
||||
original_content = content
|
||||
|
||||
# Get data
|
||||
pyproject = get_pyproject()
|
||||
commits = get_commits_since_tag(get_last_tag())
|
||||
deps = get_dependencies(pyproject)
|
||||
commands = get_available_commands()
|
||||
|
||||
# Generate changelog
|
||||
version = get_last_tag() or "v0.1.0"
|
||||
changelog = format_changelog(commits, version)
|
||||
|
||||
# Update sections
|
||||
content = update_changelog_section(content, changelog)
|
||||
content = update_dependencies_section(content, deps)
|
||||
content = update_commands_section(content, commands)
|
||||
@@ -375,7 +333,6 @@ def update_readme(check_only: bool = False) -> bool:
|
||||
print("README.md is up to date")
|
||||
return needs_update
|
||||
|
||||
# Write updated content
|
||||
if content != original_content:
|
||||
readme_path.write_text(content)
|
||||
print("README.md updated successfully")
|
||||
@@ -386,7 +343,6 @@ def update_readme(check_only: bool = False) -> bool:
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import sys
|
||||
|
||||
check_only = "--check" in sys.argv
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
# Global pytest fixtures and configuration
|
||||
# Shared across all test types
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# Disable Python bytecode cache
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop_policy():
|
||||
"""Use default event loop policy for asyncio tests."""
|
||||
import asyncio
|
||||
|
||||
return asyncio.DefaultEventLoopPolicy()
|
||||
|
||||
@@ -4,38 +4,30 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
|
||||
# Предполагаем, что тестируемый модуль называется `myapp`
|
||||
# Импортируем из него нужные объекты
|
||||
from app.main import app_factory, lifespan, main
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifespan():
|
||||
"""Проверяет, что lifespan является корректным асинхронным контекстным менеджером."""
|
||||
app = FastAPI()
|
||||
# Проверяем, что lifespan - это asynccontextmanager
|
||||
assert isinstance(lifespan, asynccontextmanager(lifespan).__class__)
|
||||
|
||||
# Проверяем, что контекстный менеджер работает (ничего не ломается)
|
||||
async with lifespan(app):
|
||||
pass # Просто убеждаемся, что yield отрабатывает
|
||||
pass
|
||||
|
||||
|
||||
def test_app_factory():
|
||||
"""Проверяет, что app_factory создаёт правильное приложение FastAPI с переданным lifespan."""
|
||||
app = app_factory()
|
||||
assert isinstance(app, FastAPI)
|
||||
# Проверяем, что lifespan приложения установлен на функцию lifespan
|
||||
assert app.router.lifespan_context == lifespan
|
||||
|
||||
|
||||
@patch("app.main.uvicorn.run")
|
||||
def test_main(mock_uvicorn_run):
|
||||
"""Проверяет, что main вызывает uvicorn.run с правильными параметрами."""
|
||||
main()
|
||||
mock_uvicorn_run.assert_called_once_with(
|
||||
app_factory,
|
||||
factory=True,
|
||||
host="0.0.0.0",
|
||||
port=8000, # Предполагаемый порт (в коде обрезано, но обычно 8000)
|
||||
port=8000,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user