ci: add ruff linting and mypy type checking

Add ruff and mypy to dev dependencies with configuration in pyproject.toml.
Add a lint CI job that runs ruff check, ruff format --check, and mypy.
Auto-fix import sorting and formatting across the codebase. Exclude
alembic/versions from linting (auto-generated migrations). Ignore B008
(FastAPI Depends pattern) and RUF001 (unicode in user-facing strings).

21 ruff lint errors and 50 mypy errors remain for manual review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Justin Visser 2026-02-26 22:11:41 +01:00
parent 399276ea69
commit bf04c77aa9
137 changed files with 3066 additions and 1900 deletions

View file

@ -1,8 +1,8 @@
from app.services.domains.dbops.application import run_publication_link_repair
from app.services.domains.dbops.integrity import collect_integrity_report
from app.services.domains.dbops.near_duplicate_repair import (
run_publication_near_duplicate_repair,
)
from app.services.domains.dbops.integrity import collect_integrity_report
from app.services.domains.dbops.query import list_repair_jobs
__all__ = [

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import delete, exists, func, select, update
@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.db.models import DataRepairJob, IngestionQueueItem, Publication, ScholarProfile, ScholarPublication
REPAIR_STATUS_PLANNED = "planned"
REPAIR_STATUS_RUNNING = "running"
REPAIR_STATUS_COMPLETED = "completed"
@ -18,7 +17,7 @@ SCOPE_MODE_ALL_USERS = "all_users"
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
return datetime.now(UTC)
def _normalize_scope_mode(scope_mode: str) -> str:
@ -105,11 +104,7 @@ async def _count_orphan_publications(db_session: AsyncSession) -> int:
stmt = (
select(func.count())
.select_from(Publication)
.where(
~exists(
select(1).where(ScholarPublication.publication_id == Publication.id)
)
)
.where(~exists(select(1).where(ScholarPublication.publication_id == Publication.id)))
)
result = await db_session.execute(stmt)
return int(result.scalar_one() or 0)
@ -158,9 +153,7 @@ def _job_summary(
async def _delete_links_for_targets(db_session: AsyncSession, *, target_scholar_profile_ids: list[int]) -> int:
result = await db_session.execute(
delete(ScholarPublication).where(
ScholarPublication.scholar_profile_id.in_(target_scholar_profile_ids)
)
delete(ScholarPublication).where(ScholarPublication.scholar_profile_id.in_(target_scholar_profile_ids))
)
return int(result.rowcount or 0)
@ -171,9 +164,7 @@ async def _delete_queue_for_targets(
user_id: int | None,
target_scholar_profile_ids: list[int],
) -> int:
stmt = delete(IngestionQueueItem).where(
IngestionQueueItem.scholar_profile_id.in_(target_scholar_profile_ids)
)
stmt = delete(IngestionQueueItem).where(IngestionQueueItem.scholar_profile_id.in_(target_scholar_profile_ids))
if user_id is not None:
stmt = stmt.where(IngestionQueueItem.user_id == user_id)
result = await db_session.execute(stmt)
@ -203,9 +194,7 @@ async def _reset_scholar_tracking_state(
async def _delete_orphan_publications(db_session: AsyncSession) -> int:
result = await db_session.execute(
delete(Publication).where(
~exists(select(1).where(ScholarPublication.publication_id == Publication.id))
)
delete(Publication).where(~exists(select(1).where(ScholarPublication.publication_id == Publication.id)))
)
return int(result.rowcount or 0)

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import func, select, text
@ -168,7 +168,7 @@ async def collect_integrity_report(db_session: AsyncSession) -> dict[str, Any]:
return {
"status": _status_from_issues(failures=failures, warnings=warnings),
"checked_at": datetime.now(timezone.utc).isoformat(),
"checked_at": datetime.now(UTC).isoformat(),
"failures": failures,
"warnings": warnings,
"checks": checks,

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
@ -17,7 +17,7 @@ NEAR_DUP_DEFAULT_MAX_CLUSTERS = 25
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
return datetime.now(UTC)
def _normalized_cluster_keys(values: list[str] | None) -> list[str]:

View file

@ -16,7 +16,5 @@ async def list_repair_jobs(
limit: int = 50,
) -> list[DataRepairJob]:
bounded = _bounded_limit(limit)
result = await db_session.execute(
select(DataRepairJob).order_by(DataRepairJob.created_at.desc()).limit(bounded)
)
result = await db_session.execute(select(DataRepairJob).order_by(DataRepairJob.created_at.desc()).limit(bounded))
return list(result.scalars())