Add pyproject.toml for consolidated project configuration and replace Black, isort, and Flake8 with Ruff

- Introduced `pyproject.toml` to centralize backend tool configurations (e.g., Ruff, mypy, coverage, pytest).
- Replaced Black, isort, and Flake8 with Ruff for linting, formatting, and import sorting.
- Updated `requirements.txt` to include Ruff and remove replaced tools.
- Added `Makefile` to streamline development workflows with commands for linting, formatting, type-checking, testing, and cleanup.
This commit is contained in:
2025-11-10 11:55:15 +01:00
parent a5c671c133
commit c589b565f0
86 changed files with 4572 additions and 3956 deletions

View File

@@ -1,18 +1,17 @@
# app/services/auth_service.py
import logging
from typing import Optional
from uuid import UUID
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.auth import (
verify_password_async,
get_password_hash_async,
TokenExpiredError,
TokenInvalidError,
create_access_token,
create_refresh_token,
TokenExpiredError,
TokenInvalidError
get_password_hash_async,
verify_password_async,
)
from app.core.config import settings
from app.core.exceptions import AuthenticationError
@@ -26,7 +25,9 @@ class AuthService:
"""Service for handling authentication operations"""
@staticmethod
async def authenticate_user(db: AsyncSession, email: str, password: str) -> Optional[User]:
async def authenticate_user(
db: AsyncSession, email: str, password: str
) -> User | None:
"""
Authenticate a user with email and password using async password verification.
@@ -87,7 +88,7 @@ class AuthService:
last_name=user_data.last_name,
phone_number=user_data.phone_number,
is_active=True,
is_superuser=False
is_superuser=False,
)
db.add(user)
@@ -103,8 +104,8 @@ class AuthService:
except Exception as e:
# Rollback on any database errors
await db.rollback()
logger.error(f"Error creating user: {str(e)}", exc_info=True)
raise AuthenticationError(f"Failed to create user: {str(e)}")
logger.error(f"Error creating user: {e!s}", exc_info=True)
raise AuthenticationError(f"Failed to create user: {e!s}")
@staticmethod
def create_tokens(user: User) -> Token:
@@ -121,18 +122,13 @@ class AuthService:
claims = {
"is_superuser": user.is_superuser,
"email": user.email,
"first_name": user.first_name
"first_name": user.first_name,
}
# Create tokens
access_token = create_access_token(
subject=str(user.id),
claims=claims
)
access_token = create_access_token(subject=str(user.id), claims=claims)
refresh_token = create_refresh_token(
subject=str(user.id)
)
refresh_token = create_refresh_token(subject=str(user.id))
# Convert User model to UserResponse schema
user_response = UserResponse.model_validate(user)
@@ -141,7 +137,8 @@ class AuthService:
access_token=access_token,
refresh_token=refresh_token,
user=user_response,
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 # Convert minutes to seconds
expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES
* 60, # Convert minutes to seconds
)
@staticmethod
@@ -180,11 +177,13 @@ class AuthService:
return AuthService.create_tokens(user)
except (TokenExpiredError, TokenInvalidError) as e:
logger.warning(f"Token refresh failed: {str(e)}")
logger.warning(f"Token refresh failed: {e!s}")
raise
@staticmethod
async def change_password(db: AsyncSession, user_id: UUID, current_password: str, new_password: str) -> bool:
async def change_password(
db: AsyncSession, user_id: UUID, current_password: str, new_password: str
) -> bool:
"""
Change a user's password.
@@ -223,5 +222,7 @@ class AuthService:
except Exception as e:
# Rollback on any database errors
await db.rollback()
logger.error(f"Error changing password for user {user_id}: {str(e)}", exc_info=True)
raise AuthenticationError(f"Failed to change password: {str(e)}")
logger.error(
f"Error changing password for user {user_id}: {e!s}", exc_info=True
)
raise AuthenticationError(f"Failed to change password: {e!s}")

View File

@@ -5,9 +5,9 @@ Email service with placeholder implementation.
This service provides email sending functionality with a simple console/log-based
placeholder that can be easily replaced with a real email provider (SendGrid, SES, etc.)
"""
import logging
from abc import ABC, abstractmethod
from typing import List, Optional
from app.core.config import settings
@@ -20,13 +20,12 @@ class EmailBackend(ABC):
@abstractmethod
async def send_email(
self,
to: List[str],
to: list[str],
subject: str,
html_content: str,
text_content: Optional[str] = None
text_content: str | None = None,
) -> bool:
"""Send an email."""
pass
class ConsoleEmailBackend(EmailBackend):
@@ -39,10 +38,10 @@ class ConsoleEmailBackend(EmailBackend):
async def send_email(
self,
to: List[str],
to: list[str],
subject: str,
html_content: str,
text_content: Optional[str] = None
text_content: str | None = None,
) -> bool:
"""
Log email content to console/logs.
@@ -88,10 +87,10 @@ class SMTPEmailBackend(EmailBackend):
async def send_email(
self,
to: List[str],
to: list[str],
subject: str,
html_content: str,
text_content: Optional[str] = None
text_content: str | None = None,
) -> bool:
"""Send email via SMTP."""
# TODO: Implement SMTP sending
@@ -108,7 +107,7 @@ class EmailService:
and can be configured to use different backends (console, SMTP, SendGrid, etc.)
"""
def __init__(self, backend: Optional[EmailBackend] = None):
def __init__(self, backend: EmailBackend | None = None):
"""
Initialize email service with a backend.
@@ -118,10 +117,7 @@ class EmailService:
self.backend = backend or ConsoleEmailBackend()
async def send_password_reset_email(
self,
to_email: str,
reset_token: str,
user_name: Optional[str] = None
self, to_email: str, reset_token: str, user_name: str | None = None
) -> bool:
"""
Send password reset email.
@@ -142,7 +138,7 @@ class EmailService:
# Plain text version
text_content = f"""
Hello{' ' + user_name if user_name else ''},
Hello{" " + user_name if user_name else ""},
You requested a password reset for your account. Click the link below to reset your password:
@@ -177,7 +173,7 @@ The {settings.PROJECT_NAME} Team
<h1>Password Reset</h1>
</div>
<div class="content">
<p>Hello{' ' + user_name if user_name else ''},</p>
<p>Hello{" " + user_name if user_name else ""},</p>
<p>You requested a password reset for your account. Click the button below to reset your password:</p>
<p style="text-align: center;">
<a href="{reset_url}" class="button">Reset Password</a>
@@ -200,17 +196,14 @@ The {settings.PROJECT_NAME} Team
to=[to_email],
subject=subject,
html_content=html_content,
text_content=text_content
text_content=text_content,
)
except Exception as e:
logger.error(f"Failed to send password reset email to {to_email}: {str(e)}")
logger.error(f"Failed to send password reset email to {to_email}: {e!s}")
return False
async def send_email_verification(
self,
to_email: str,
verification_token: str,
user_name: Optional[str] = None
self, to_email: str, verification_token: str, user_name: str | None = None
) -> bool:
"""
Send email verification email.
@@ -224,14 +217,16 @@ The {settings.PROJECT_NAME} Team
True if email sent successfully
"""
# Generate verification URL
verification_url = f"{settings.FRONTEND_URL}/verify-email?token={verification_token}"
verification_url = (
f"{settings.FRONTEND_URL}/verify-email?token={verification_token}"
)
# Prepare email content
subject = "Verify Your Email Address"
# Plain text version
text_content = f"""
Hello{' ' + user_name if user_name else ''},
Hello{" " + user_name if user_name else ""},
Thank you for signing up! Please verify your email address by clicking the link below:
@@ -266,7 +261,7 @@ The {settings.PROJECT_NAME} Team
<h1>Verify Your Email</h1>
</div>
<div class="content">
<p>Hello{' ' + user_name if user_name else ''},</p>
<p>Hello{" " + user_name if user_name else ""},</p>
<p>Thank you for signing up! Please verify your email address by clicking the button below:</p>
<p style="text-align: center;">
<a href="{verification_url}" class="button">Verify Email</a>
@@ -289,10 +284,10 @@ The {settings.PROJECT_NAME} Team
to=[to_email],
subject=subject,
html_content=html_content,
text_content=text_content
text_content=text_content,
)
except Exception as e:
logger.error(f"Failed to send verification email to {to_email}: {str(e)}")
logger.error(f"Failed to send verification email to {to_email}: {e!s}")
return False

View File

@@ -3,8 +3,9 @@ Background job for cleaning up expired sessions.
This service runs periodically to remove old session records from the database.
"""
import logging
from datetime import datetime, timezone
from datetime import UTC, datetime
from app.core.database import SessionLocal
from app.crud.session import session as session_crud
@@ -39,7 +40,7 @@ async def cleanup_expired_sessions(keep_days: int = 30) -> int:
return count
except Exception as e:
logger.error(f"Error during session cleanup: {str(e)}", exc_info=True)
logger.error(f"Error during session cleanup: {e!s}", exc_info=True)
return 0
@@ -52,20 +53,21 @@ async def get_session_statistics() -> dict:
"""
async with SessionLocal() as db:
try:
from sqlalchemy import func, select
from app.models.user_session import UserSession
from sqlalchemy import select, func
total_result = await db.execute(select(func.count(UserSession.id)))
total_sessions = total_result.scalar_one()
active_result = await db.execute(
select(func.count(UserSession.id)).where(UserSession.is_active == True)
select(func.count(UserSession.id)).where(UserSession.is_active)
)
active_sessions = active_result.scalar_one()
expired_result = await db.execute(
select(func.count(UserSession.id)).where(
UserSession.expires_at < datetime.now(timezone.utc)
UserSession.expires_at < datetime.now(UTC)
)
)
expired_sessions = expired_result.scalar_one()
@@ -82,5 +84,5 @@ async def get_session_statistics() -> dict:
return stats
except Exception as e:
logger.error(f"Error getting session statistics: {str(e)}", exc_info=True)
logger.error(f"Error getting session statistics: {e!s}", exc_info=True)
return {}