Compare commits
6 Commits
c589b565f0
...
5c47be2ee5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c47be2ee5 | ||
|
|
e9f787040a | ||
|
|
2532d1ac3c | ||
|
|
1f45ca2b50 | ||
|
|
8a343580ce | ||
|
|
424ca166b8 |
@@ -1,5 +1,13 @@
|
|||||||
.PHONY: help lint lint-fix format format-check type-check test test-cov validate clean install-dev
|
.PHONY: help lint lint-fix format format-check type-check test test-cov validate clean install-dev
|
||||||
|
|
||||||
|
# Virtual environment binaries
|
||||||
|
VENV_BIN = .venv/bin
|
||||||
|
PYTHON = $(VENV_BIN)/python
|
||||||
|
PIP = $(VENV_BIN)/pip
|
||||||
|
PYTEST = $(VENV_BIN)/pytest
|
||||||
|
RUFF = $(VENV_BIN)/ruff
|
||||||
|
MYPY = $(VENV_BIN)/mypy
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
help:
|
help:
|
||||||
@echo "🚀 FastAPI Backend - Development Commands"
|
@echo "🚀 FastAPI Backend - Development Commands"
|
||||||
@@ -26,23 +34,23 @@ help:
|
|||||||
|
|
||||||
lint:
|
lint:
|
||||||
@echo "🔍 Running Ruff linter..."
|
@echo "🔍 Running Ruff linter..."
|
||||||
@ruff check app/ tests/
|
@$(RUFF) check app/ tests/
|
||||||
|
|
||||||
lint-fix:
|
lint-fix:
|
||||||
@echo "🔧 Running Ruff linter with auto-fix..."
|
@echo "🔧 Running Ruff linter with auto-fix..."
|
||||||
@ruff check --fix app/ tests/
|
@$(RUFF) check --fix app/ tests/
|
||||||
|
|
||||||
format:
|
format:
|
||||||
@echo "✨ Formatting code with Ruff..."
|
@echo "✨ Formatting code with Ruff..."
|
||||||
@ruff format app/ tests/
|
@$(RUFF) format app/ tests/
|
||||||
|
|
||||||
format-check:
|
format-check:
|
||||||
@echo "📋 Checking code formatting..."
|
@echo "📋 Checking code formatting..."
|
||||||
@ruff format --check app/ tests/
|
@$(RUFF) format --check app/ tests/
|
||||||
|
|
||||||
type-check:
|
type-check:
|
||||||
@echo "🔎 Running mypy type checking..."
|
@echo "🔎 Running mypy type checking..."
|
||||||
@mypy app/
|
@$(MYPY) app/
|
||||||
|
|
||||||
validate: lint format-check type-check
|
validate: lint format-check type-check
|
||||||
@echo "✅ All quality checks passed!"
|
@echo "✅ All quality checks passed!"
|
||||||
@@ -53,11 +61,11 @@ validate: lint format-check type-check
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
@echo "🧪 Running tests..."
|
@echo "🧪 Running tests..."
|
||||||
@IS_TEST=True pytest
|
@IS_TEST=True PYTHONPATH=. $(PYTEST)
|
||||||
|
|
||||||
test-cov:
|
test-cov:
|
||||||
@echo "🧪 Running tests with coverage..."
|
@echo "🧪 Running tests with coverage..."
|
||||||
@IS_TEST=True pytest --cov=app --cov-report=term-missing --cov-report=html -n 0
|
@IS_TEST=True PYTHONPATH=. $(PYTEST) --cov=app --cov-report=term-missing --cov-report=html -n 16
|
||||||
@echo "📊 Coverage report generated in htmlcov/index.html"
|
@echo "📊 Coverage report generated in htmlcov/index.html"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -66,7 +74,7 @@ test-cov:
|
|||||||
|
|
||||||
install-dev:
|
install-dev:
|
||||||
@echo "📦 Installing development dependencies..."
|
@echo "📦 Installing development dependencies..."
|
||||||
@pip install -r requirements.txt
|
@$(PIP) install -r requirements.txt
|
||||||
@echo "✅ Development environment ready!"
|
@echo "✅ Development environment ready!"
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
@@ -72,8 +72,11 @@ async def list_my_sessions(
|
|||||||
decode_token(access_token)
|
decode_token(access_token)
|
||||||
# Note: Access tokens don't have JTI by default, but we can try
|
# Note: Access tokens don't have JTI by default, but we can try
|
||||||
# For now, we'll mark current based on most recent activity
|
# For now, we'll mark current based on most recent activity
|
||||||
except Exception:
|
except Exception as e:
|
||||||
pass
|
# Optional token parsing - silently ignore failures
|
||||||
|
logger.debug(
|
||||||
|
f"Failed to decode access token for session marking: {e!s}"
|
||||||
|
)
|
||||||
|
|
||||||
# Convert to response format
|
# Convert to response format
|
||||||
session_responses = []
|
session_responses = []
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
logging.getLogger("passlib").setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
@@ -15,6 +12,9 @@ from pydantic import ValidationError
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.schemas.users import TokenData, TokenPayload
|
from app.schemas.users import TokenData, TokenPayload
|
||||||
|
|
||||||
|
# Suppress passlib bcrypt warnings about ident
|
||||||
|
logging.getLogger("passlib").setLevel(logging.ERROR)
|
||||||
|
|
||||||
# Password hashing context
|
# Password hashing context
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ def create_async_production_engine() -> AsyncEngine:
|
|||||||
|
|
||||||
# Add PostgreSQL-specific connect_args
|
# Add PostgreSQL-specific connect_args
|
||||||
if "postgresql" in async_url:
|
if "postgresql" in async_url:
|
||||||
engine_config["connect_args"] = {
|
engine_config["connect_args"] = { # type: ignore[assignment]
|
||||||
"server_settings": {
|
"server_settings": {
|
||||||
"application_name": settings.PROJECT_NAME,
|
"application_name": settings.PROJECT_NAME,
|
||||||
"timezone": "UTC",
|
"timezone": "UTC",
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ class CRUDSession(CRUDBase[UserSession, SessionCreate, SessionUpdate]):
|
|||||||
# Use bulk DELETE with WHERE clause - single query
|
# Use bulk DELETE with WHERE clause - single query
|
||||||
stmt = delete(UserSession).where(
|
stmt = delete(UserSession).where(
|
||||||
and_(
|
and_(
|
||||||
not UserSession.is_active,
|
UserSession.is_active == False, # noqa: E712
|
||||||
UserSession.expires_at < now,
|
UserSession.expires_at < now,
|
||||||
UserSession.created_at < cutoff_date,
|
UserSession.created_at < cutoff_date,
|
||||||
)
|
)
|
||||||
@@ -356,7 +356,7 @@ class CRUDSession(CRUDBase[UserSession, SessionCreate, SessionUpdate]):
|
|||||||
stmt = delete(UserSession).where(
|
stmt = delete(UserSession).where(
|
||||||
and_(
|
and_(
|
||||||
UserSession.user_id == uuid_obj,
|
UserSession.user_id == uuid_obj,
|
||||||
not UserSession.is_active,
|
UserSession.is_active == False, # noqa: E712
|
||||||
UserSession.expires_at < now,
|
UserSession.expires_at < now,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from sqlalchemy import Column, DateTime
|
|||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
|
from app.core.database import Base # Re-exported for other models
|
||||||
|
|
||||||
|
|
||||||
class TimestampMixin:
|
class TimestampMixin:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class UserOrganization(Base, TimestampMixin):
|
|||||||
primary_key=True,
|
primary_key=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
role = Column(
|
role: Column[OrganizationRole] = Column(
|
||||||
Enum(OrganizationRole),
|
Enum(OrganizationRole),
|
||||||
default=OrganizationRole.MEMBER,
|
default=OrganizationRole.MEMBER,
|
||||||
nullable=False,
|
nullable=False,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ ignore = [
|
|||||||
"S603", # subprocess without shell=True (safe usage)
|
"S603", # subprocess without shell=True (safe usage)
|
||||||
"S607", # Starting a process with a partial path (safe usage)
|
"S607", # Starting a process with a partial path (safe usage)
|
||||||
"B008", # FastAPI Depends() in function defaults (required by framework)
|
"B008", # FastAPI Depends() in function defaults (required by framework)
|
||||||
|
"B904", # Exception chaining (overly strict for FastAPI error handlers)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Allow autofix for all enabled rules
|
# Allow autofix for all enabled rules
|
||||||
@@ -81,8 +82,11 @@ unfixable = []
|
|||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"app/alembic/env.py" = ["E402", "F403", "F405"] # Alembic requires specific import order
|
"app/alembic/env.py" = ["E402", "F403", "F405"] # Alembic requires specific import order
|
||||||
"app/alembic/versions/*.py" = ["E402"] # Migration files have specific structure
|
"app/alembic/versions/*.py" = ["E402"] # Migration files have specific structure
|
||||||
"tests/**/*.py" = ["S101"] # pytest uses assert statements
|
"tests/**/*.py" = ["S101", "N806", "B017", "N817", "S110", "ASYNC251", "RUF043"] # pytest: asserts, CamelCase fixtures, blind exceptions, try-pass patterns, and async test helpers are intentional
|
||||||
"app/models/__init__.py" = ["F401"] # __init__ files re-export modules
|
"app/models/__init__.py" = ["F401"] # __init__ files re-export modules
|
||||||
|
"app/models/base.py" = ["F401"] # Re-exports Base for use by other models
|
||||||
|
"app/utils/test_utils.py" = ["N806"] # SQLAlchemy session factories use CamelCase convention
|
||||||
|
"app/main.py" = ["N806"] # Constants use UPPER_CASE convention
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Ruff Import Sorting (isort replacement)
|
# Ruff Import Sorting (isort replacement)
|
||||||
@@ -114,7 +118,7 @@ line-ending = "lf"
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.12"
|
python_version = "3.12"
|
||||||
warn_return_any = true
|
warn_return_any = false # SQLAlchemy queries return Any - overly strict
|
||||||
warn_unused_configs = true
|
warn_unused_configs = true
|
||||||
disallow_untyped_defs = false # Gradual typing - enable later
|
disallow_untyped_defs = false # Gradual typing - enable later
|
||||||
disallow_incomplete_defs = false
|
disallow_incomplete_defs = false
|
||||||
@@ -136,6 +140,10 @@ plugins = ["pydantic.mypy"]
|
|||||||
module = "alembic.*"
|
module = "alembic.*"
|
||||||
ignore_errors = true
|
ignore_errors = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "app.alembic.*"
|
||||||
|
ignore_errors = true
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = "sqlalchemy.*"
|
module = "sqlalchemy.*"
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
@@ -156,6 +164,57 @@ ignore_missing_imports = true
|
|||||||
module = "passlib.*"
|
module = "passlib.*"
|
||||||
ignore_missing_imports = true
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "pydantic_settings.*"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "fastapi.*"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "apscheduler.*"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "starlette.*"
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
# SQLAlchemy ORM models - Column descriptors cause type confusion
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "app.models.*"
|
||||||
|
disable_error_code = ["assignment", "arg-type", "return-value"]
|
||||||
|
|
||||||
|
# CRUD operations - Generic ModelType and SQLAlchemy Result issues
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "app.crud.*"
|
||||||
|
disable_error_code = ["attr-defined", "assignment", "arg-type", "return-value"]
|
||||||
|
|
||||||
|
# API routes - SQLAlchemy Column to Pydantic schema conversions
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "app.api.routes.*"
|
||||||
|
disable_error_code = ["arg-type", "call-arg", "call-overload", "assignment"]
|
||||||
|
|
||||||
|
# API dependencies - Similar SQLAlchemy Column issues
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "app.api.dependencies.*"
|
||||||
|
disable_error_code = ["arg-type"]
|
||||||
|
|
||||||
|
# FastAPI exception handlers have correct signatures despite mypy warnings
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "app.main"
|
||||||
|
disable_error_code = ["arg-type"]
|
||||||
|
|
||||||
|
# Auth service - SQLAlchemy Column issues
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "app.services.auth_service"
|
||||||
|
disable_error_code = ["assignment", "arg-type"]
|
||||||
|
|
||||||
|
# Test utils - Testing patterns
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "app.utils.auth_test_utils"
|
||||||
|
disable_error_code = ["assignment", "arg-type"]
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Pydantic mypy plugin configuration
|
# Pydantic mypy plugin configuration
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user