[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] include = ["app*"] exclude = ["tests*", "uploads*", "docs*"] [project] name = "fast-next-backend" version = "0.1.0" description = "FastAPI backend for Fast-Next template" readme = "README.md" requires-python = ">=3.12" # Core dependencies dependencies = [ # Core FastAPI framework and dependencies "fastapi>=0.115.8", "uvicorn>=0.34.0", "pydantic>=2.10.6", "pydantic-settings>=2.2.1", "python-multipart>=0.0.19", "fastapi-utils==0.8.0", # Database "sqlalchemy>=2.0.29", "alembic>=1.14.1", "psycopg2-binary>=2.9.9", "asyncpg>=0.29.0", "aiosqlite==0.21.0", # Environment configuration "python-dotenv>=1.0.1", # API utilities "email-validator>=2.1.0.post1", "ujson>=5.9.0", # CORS and security "starlette>=0.40.0", "starlette-csrf>=1.4.5", "slowapi>=0.1.9", # Utilities "httpx>=0.27.0", "tenacity>=8.2.3", "pytz>=2024.1", "pillow>=10.3.0", "apscheduler==3.11.0", # Security and authentication (pinned for reproducibility) "python-jose==3.4.0", "passlib==1.7.4", "bcrypt==4.2.1", "cryptography==44.0.1", # OAuth authentication "authlib>=1.3.0", ] # Development dependencies [project.optional-dependencies] dev = [ # Testing "pytest>=8.0.0", "pytest-asyncio>=0.23.5", "pytest-cov>=4.1.0", "pytest-xdist>=3.8.0", "requests>=2.32.0", "freezegun~=1.5.1", # Development tools "ruff>=0.8.0", # All-in-one: linting, formatting, import sorting "mypy>=1.8.0", # Type checking ] # ============================================================================ # Ruff Configuration - All-in-one linting, formatting, and import sorting # ============================================================================ [tool.ruff] target-version = "py312" line-length = 88 # Black-compatible indent-width = 4 # Exclude directories exclude = [ ".git", ".venv", "__pycache__", "*.egg-info", ".pytest_cache", ".mypy_cache", ".ruff_cache", "alembic/versions", # Generated migration files ] # ============================================================================ # Ruff Linting Rules # ============================================================================ [tool.ruff.lint] # Enable these rule sets: # E/W - pycodestyle errors and warnings # F - pyflakes (unused imports, variables, etc.) # I - isort (import sorting) # N - pep8-naming (naming conventions) # UP - pyupgrade (modern Python syntax) # B - flake8-bugbear (common bugs) # C4 - flake8-comprehensions (list/dict comprehensions) # PIE - flake8-pie (misc lints) # RUF - Ruff-specific rules # ASYNC - flake8-async (async best practices) # S - flake8-bandit (security) # T20 - flake8-print (no print statements) select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "N", # pep8-naming "UP", # pyupgrade "B", # flake8-bugbear "C4", # flake8-comprehensions "PIE", # flake8-pie "RUF", # Ruff-specific "ASYNC", # flake8-async "S", # flake8-bandit (security) ] # Ignore specific rules ignore = [ "E501", # Line too long (handled by formatter) "S101", # Use of assert (pytest uses asserts) "S104", # Possible binding to all interfaces (FastAPI needs 0.0.0.0) "S105", # Possible hardcoded password (false positives in field names) "S106", # Possible hardcoded password (false positives in field names) "S603", # subprocess without shell=True (safe usage) "S607", # Starting a process with a partial path (safe usage) "B008", # FastAPI Depends() in function defaults (required by framework) "B904", # Exception chaining (overly strict for FastAPI error handlers) ] # Allow autofix for all enabled rules fixable = ["ALL"] unfixable = [] # Per-file ignores for special cases [tool.ruff.lint.per-file-ignores] "app/alembic/env.py" = ["E402", "F403", "F405"] # Alembic requires specific import order "app/alembic/versions/*.py" = ["E402"] # Migration files have specific structure "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/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) # ============================================================================ [tool.ruff.lint.isort] known-first-party = ["app", "tests"] section-order = [ "future", "standard-library", "third-party", "first-party", "local-folder", ] combine-as-imports = true force-wrap-aliases = true split-on-trailing-comma = true # ============================================================================ # Ruff Formatting (Black replacement) # ============================================================================ [tool.ruff.format] quote-style = "double" indent-style = "space" skip-magic-trailing-comma = false line-ending = "lf" # ============================================================================ # mypy Configuration - Type Checking # ============================================================================ [tool.mypy] python_version = "3.12" warn_return_any = false # SQLAlchemy queries return Any - overly strict warn_unused_configs = true disallow_untyped_defs = false # Gradual typing - enable later disallow_incomplete_defs = false check_untyped_defs = true no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true warn_no_return = true strict_equality = true ignore_missing_imports = false explicit_package_bases = true namespace_packages = true # Pydantic plugin for better validation plugins = ["pydantic.mypy"] # Per-module options [[tool.mypy.overrides]] module = "alembic.*" ignore_errors = true [[tool.mypy.overrides]] module = "app.alembic.*" ignore_errors = true [[tool.mypy.overrides]] module = "sqlalchemy.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "fastapi_utils.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "slowapi.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "jose.*" ignore_missing_imports = true [[tool.mypy.overrides]] module = "passlib.*" 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 [[tool.mypy.overrides]] module = "authlib.*" 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 # ============================================================================ [tool.pydantic-mypy] init_forbid_extra = true init_typed = true warn_required_dynamic_aliases = true # ============================================================================ # Pytest Configuration # ============================================================================ [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = [ "--disable-warnings", "-n", "auto", # parallel execution "--strict-markers", "--tb=short", "--cov=app", "--cov-report=term-missing", "--cov-report=html", ] markers = [ "sqlite: marks tests that should run on SQLite (mocked).", "postgres: marks tests that require a real PostgreSQL database.", ] asyncio_default_fixture_loop_scope = "function" # ============================================================================ # Coverage Configuration # ============================================================================ [tool.coverage.run] source = ["app"] omit = [ "*/tests/*", "*/__pycache__/*", "*/alembic/versions/*", "*/.venv/*", "app/init_db.py", # CLI script for database initialization ] branch = true [tool.coverage.report] precision = 2 exclude_lines = [ "pragma: no cover", "def __repr__", "raise AssertionError", "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", "@abstractmethod", ]