diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..a77894b --- /dev/null +++ b/.flake8 @@ -0,0 +1,21 @@ +[flake8] +# Keep in sync with black's line-length in pyproject.toml [tool.black] +max-line-length = 100 +max-complexity = 10 +exclude = + .git, + __pycache__, + .venv, + venv, + build, + dist, + *.egg-info, + migrations +# Ignored rules – must not conflict with black: +# E203 – whitespace before ':' (black formats slices this way) +# W503 – line break before binary operator (black prefers this style) +# E501 – line too long (black already enforces max-line-length; avoid double-reporting) +extend-ignore = E203, W503, E501 +per-file-ignores = + # Allow unused imports in __init__.py (re-exports) + __init__.py: F401 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d42973..390698b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,11 +80,11 @@ jobs: - name: Run Flake8 run: | - flake8 backend/app --max-line-length=100 --extend-ignore=E203,W503,E501 + flake8 backend/app - name: Run Pylint run: | - pylint backend/app --max-line-length=100 --disable=C0111,R0903 + pylint backend/app continue-on-error: true docker: diff --git a/backend/app/api/api_v1/api.py b/backend/app/api/api_v1/api.py index 9bff591..0341762 100644 --- a/backend/app/api/api_v1/api.py +++ b/backend/app/api/api_v1/api.py @@ -1,6 +1,7 @@ -from app.api.api_v1.endpoints import domains, health, imap, reports, setup, stats from fastapi import APIRouter +from app.api.api_v1.endpoints import domains, health, imap, reports, setup, stats + api_router = APIRouter() # Include all endpoint routers diff --git a/backend/app/api/api_v1/endpoints/domains.py b/backend/app/api/api_v1/endpoints/domains.py index d897871..0a52866 100644 --- a/backend/app/api/api_v1/endpoints/domains.py +++ b/backend/app/api/api_v1/endpoints/domains.py @@ -2,10 +2,11 @@ import random # Used for mock data generation - TODO: Replace with actual histo from datetime import datetime, timedelta from typing import Any, Dict, List, Optional -from app.services.report_store import ReportStore from fastapi import APIRouter, HTTPException, Path, Query, status from pydantic import BaseModel +from app.services.report_store import ReportStore + router = APIRouter() diff --git a/backend/app/api/api_v1/endpoints/health.py b/backend/app/api/api_v1/endpoints/health.py index f6e9413..a721120 100644 --- a/backend/app/api/api_v1/endpoints/health.py +++ b/backend/app/api/api_v1/endpoints/health.py @@ -1,6 +1,7 @@ -from app.api.api_v1.endpoints.setup import setup_status from fastapi import APIRouter +from app.api.api_v1.endpoints.setup import setup_status + router = APIRouter() diff --git a/backend/app/api/api_v1/endpoints/imap.py b/backend/app/api/api_v1/endpoints/imap.py index b66328a..f5cf803 100644 --- a/backend/app/api/api_v1/endpoints/imap.py +++ b/backend/app/api/api_v1/endpoints/imap.py @@ -2,11 +2,12 @@ import logging from datetime import datetime from typing import Any, Dict, Optional -from app.core.security import require_admin_auth -from app.services.imap_client import IMAPClient from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException from pydantic import BaseModel +from app.core.security import require_admin_auth +from app.services.imap_client import IMAPClient + router = APIRouter() logger = logging.getLogger(__name__) diff --git a/backend/app/api/api_v1/endpoints/reports.py b/backend/app/api/api_v1/endpoints/reports.py index 39b9ff0..a83d5bf 100644 --- a/backend/app/api/api_v1/endpoints/reports.py +++ b/backend/app/api/api_v1/endpoints/reports.py @@ -1,11 +1,12 @@ import logging from typing import List +from fastapi import APIRouter, File, HTTPException, UploadFile, status +from pydantic import BaseModel + from app.services.dmarc_parser import DMARCParser from app.services.report_store import ReportStore from app.utils.domain_validator import DomainValidationError, validate_domain -from fastapi import APIRouter, File, HTTPException, UploadFile, status -from pydantic import BaseModel logger = logging.getLogger(__name__) diff --git a/backend/app/api/api_v1/endpoints/stats.py b/backend/app/api/api_v1/endpoints/stats.py index f7ec140..7863b1a 100644 --- a/backend/app/api/api_v1/endpoints/stats.py +++ b/backend/app/api/api_v1/endpoints/stats.py @@ -1,9 +1,10 @@ from typing import Any, Dict +from fastapi import APIRouter, Depends, Path, Query +from sqlalchemy.orm import Session + from app.core.database import get_db from app.utils.stats_summarizer import StatsSummarizer -from fastapi import APIRouter, Depends, Path, Query -from sqlalchemy.orm import Session router = APIRouter() diff --git a/backend/app/core/config.py b/backend/app/core/config.py index fdc01fc..717c513 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -74,9 +74,9 @@ class Settings(BaseSettings): return v @validator("BACKEND_CORS_ORIGINS", pre=True) - def assemble_cors_origins( + def assemble_cors_origins( # pylint: disable=no-self-argument cls, v: Union[str, List[str]] - ) -> List[str]: # pylint: disable=no-self-argument + ) -> List[str]: if isinstance(v, str) and not v.startswith("["): return [i.strip() for i in v.split(",")] if isinstance(v, (list, str)): diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 96329ab..3e809fe 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -1,10 +1,11 @@ from typing import Generator -from app.core.config import get_settings from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker +from app.core.config import get_settings + settings = get_settings() # Configure SQLAlchemy diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 56836ca..2f2e76e 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -4,12 +4,13 @@ import secrets from datetime import datetime, timedelta from typing import Any, Optional, Union -from app.core.config import get_settings from fastapi import HTTPException, Security, status from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer from jose import JWTError, jwt from passlib.context import CryptContext +from app.core.config import get_settings + settings = get_settings() logger = logging.getLogger(__name__) diff --git a/backend/app/main.py b/backend/app/main.py index 0bb6eed..169e51a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -3,17 +3,18 @@ import logging import os from datetime import datetime +from fastapi import Depends, FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + from app.api.api_v1.api import api_router from app.core.config import get_settings from app.core.security import add_api_key, generate_api_key, require_admin_auth from app.middleware.security import SecurityHeadersMiddleware from app.services.imap_client import IMAPClient from app.services.report_store import ReportStore -from fastapi import Depends, FastAPI, Request -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates # Set up logging logger = logging.getLogger(__name__) diff --git a/backend/app/models/domain.py b/backend/app/models/domain.py index 1c46c31..9c61c91 100644 --- a/backend/app/models/domain.py +++ b/backend/app/models/domain.py @@ -1,9 +1,10 @@ from datetime import datetime -from app.core.database import Base from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Index, Integer, String, Text from sqlalchemy.orm import relationship +from app.core.database import Base + class Domain(Base): """Domain model representing a monitored domain""" diff --git a/backend/app/models/report.py b/backend/app/models/report.py index 612e867..3099054 100644 --- a/backend/app/models/report.py +++ b/backend/app/models/report.py @@ -1,9 +1,10 @@ from datetime import datetime -from app.core.database import Base from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String, Text from sqlalchemy.orm import relationship +from app.core.database import Base + class DMARCReport(Base): """DMARC Aggregate Report model""" diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 2e2ff25..1370a83 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,7 +1,8 @@ -from app.core.database import Base from sqlalchemy import Boolean, Column, Integer, String from sqlalchemy.orm import relationship +from app.core.database import Base + class User(Base): """User model""" diff --git a/backend/app/tests/conftest.py b/backend/app/tests/conftest.py index 61088df..fe182f4 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/tests/conftest.py @@ -1,16 +1,17 @@ # Import all models so Base.metadata knows every table -import app.models.domain # noqa: F401 # pylint: disable=unused-import -import app.models.report # noqa: F401 # pylint: disable=unused-import -import app.models.user # noqa: F401 # pylint: disable=unused-import import pytest -from app.core.database import Base, get_db -from app.main import create_app -from app.services.report_store import ReportStore from fastapi import FastAPI from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +import app.models.domain # noqa: F401 # pylint: disable=unused-import +import app.models.report # noqa: F401 # pylint: disable=unused-import +import app.models.user # noqa: F401 # pylint: disable=unused-import +from app.core.database import Base, get_db +from app.main import create_app +from app.services.report_store import ReportStore + @pytest.fixture() def test_app() -> FastAPI: diff --git a/backend/app/tests/test_dmarc_parser.py b/backend/app/tests/test_dmarc_parser.py index 72a3934..8361d52 100644 --- a/backend/app/tests/test_dmarc_parser.py +++ b/backend/app/tests/test_dmarc_parser.py @@ -2,6 +2,7 @@ import io import zipfile import pytest + from app.services.dmarc_parser import DMARCParser from app.tests.test_data import SAMPLE_XML diff --git a/backend/app/tests/test_models.py b/backend/app/tests/test_models.py index 43703b4..a6df613 100644 --- a/backend/app/tests/test_models.py +++ b/backend/app/tests/test_models.py @@ -1,6 +1,7 @@ +from sqlalchemy.orm import Session + from app.models.domain import Domain from app.models.report import DMARCReport, ReportRecord -from sqlalchemy.orm import Session class TestDomainModel: diff --git a/backend/app/tests/test_reports_api.py b/backend/app/tests/test_reports_api.py index f7a3a3a..fb77ccf 100644 --- a/backend/app/tests/test_reports_api.py +++ b/backend/app/tests/test_reports_api.py @@ -1,9 +1,10 @@ import io import zipfile -from app.tests.test_data import SAMPLE_XML from fastapi.testclient import TestClient +from app.tests.test_data import SAMPLE_XML + def _make_zip(xml_content: str) -> bytes: """Create a ZIP file containing the given XML content.""" diff --git a/backend/app/tests/test_security.py b/backend/app/tests/test_security.py index d99bebe..f83b477 100644 --- a/backend/app/tests/test_security.py +++ b/backend/app/tests/test_security.py @@ -4,8 +4,9 @@ Security-focused tests for DMARQ application. Covers API key management, domain validation, file upload limits, and XML parsing security. """ -import app.services.dmarc_parser as parser_module import pytest + +import app.services.dmarc_parser as parser_module from app.core.security import add_api_key, generate_api_key, verify_api_key from app.services.dmarc_parser import DMARCParser from app.utils.domain_validator import validate_domain, validate_domain_config diff --git a/pyproject.toml b/pyproject.toml index ddb31ed..e131f85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,24 @@ include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true +known_first_party = ["app"] +skip = ["venv", ".venv", "migrations"] + +[tool.pylint.main] +# Run from repo root: pylint backend/app +max-line-length = 100 + +[tool.pylint."messages control"] +disable = [ + "C0111", # missing-docstring + "C0103", # invalid-name (e.g. SessionLocal, TestingSessionLocal) + "R0903", # too-few-public-methods + "R0913", # too-many-arguments + "W0212", # protected-access +] + +[tool.pylint.basic] +good-names = ["i", "j", "k", "ex", "_", "id", "db"] [tool.pytest.ini_options] testpaths = ["backend/app/tests"] diff --git a/setup.cfg b/setup.cfg index 92fe295..ef8919e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,59 +31,3 @@ skip_covered = False [coverage:html] directory = htmlcov - -[flake8] -max-line-length = 100 -exclude = - .git, - __pycache__, - .venv, - venv, - build, - dist, - *.egg-info, - migrations -extend-ignore = E203, W503, E501 -per-file-ignores = - __init__.py:F401 -max-complexity = 10 - -[mypy] -python_version = 3.10 -warn_return_any = True -warn_unused_configs = True -disallow_untyped_defs = False -disallow_incomplete_defs = False -check_untyped_defs = True -disallow_untyped_calls = False -disallow_any_generics = False -ignore_missing_imports = True -no_implicit_optional = True -warn_redundant_casts = True -warn_unused_ignores = True -warn_no_return = True -strict_optional = True - -[isort] -profile = black -line_length = 100 -multi_line_output = 3 -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -ensure_newline_before_comments = True -skip = venv,.venv,migrations - -[pylint] -max-line-length = 100 -disable = - C0111, # missing-docstring - C0103, # invalid-name - R0903, # too-few-public-methods - R0913, # too-many-arguments - W0212, # protected-access -good-names = i,j,k,ex,_,id,db - -[bandit] -exclude_dirs = /tests/,/venv/,.venv/ -skips = B101,B601