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:
parent
399276ea69
commit
bf04c77aa9
137 changed files with 3066 additions and 1900 deletions
|
|
@ -1,21 +1,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from app.services.domains.publications.counts import (
|
||||
count_for_user,
|
||||
count_favorite_for_user,
|
||||
count_for_user,
|
||||
count_latest_for_user,
|
||||
count_unread_for_user,
|
||||
)
|
||||
from app.services.domains.publications.listing import (
|
||||
list_for_user,
|
||||
retry_pdf_for_user,
|
||||
list_unread_for_user,
|
||||
)
|
||||
from app.services.domains.publications.enrichment import (
|
||||
hydrate_pdf_enrichment_state,
|
||||
schedule_missing_pdf_enrichment_for_user,
|
||||
schedule_retry_pdf_enrichment_for_row,
|
||||
)
|
||||
from app.services.domains.publications.listing import (
|
||||
list_for_user,
|
||||
list_unread_for_user,
|
||||
retry_pdf_for_user,
|
||||
)
|
||||
from app.services.domains.publications.modes import (
|
||||
MODE_ALL,
|
||||
MODE_LATEST,
|
||||
|
|
@ -23,6 +23,13 @@ from app.services.domains.publications.modes import (
|
|||
MODE_UNREAD,
|
||||
resolve_publication_view_mode,
|
||||
)
|
||||
from app.services.domains.publications.pdf_queue import (
|
||||
count_pdf_queue_items,
|
||||
enqueue_all_missing_pdf_jobs,
|
||||
enqueue_retry_pdf_job_for_publication_id,
|
||||
list_pdf_queue_items,
|
||||
list_pdf_queue_page,
|
||||
)
|
||||
from app.services.domains.publications.queries import (
|
||||
get_latest_run_id_for_user,
|
||||
get_publication_item_for_user,
|
||||
|
|
@ -33,42 +40,35 @@ from app.services.domains.publications.read_state import (
|
|||
mark_selected_as_read_for_user,
|
||||
set_publication_favorite_for_user,
|
||||
)
|
||||
from app.services.domains.publications.pdf_queue import (
|
||||
count_pdf_queue_items,
|
||||
enqueue_all_missing_pdf_jobs,
|
||||
enqueue_retry_pdf_job_for_publication_id,
|
||||
list_pdf_queue_page,
|
||||
list_pdf_queue_items,
|
||||
)
|
||||
from app.services.domains.publications.types import PublicationListItem, UnreadPublicationItem
|
||||
|
||||
__all__ = [
|
||||
"MODE_ALL",
|
||||
"MODE_UNREAD",
|
||||
"MODE_LATEST",
|
||||
"MODE_NEW",
|
||||
"MODE_UNREAD",
|
||||
"PublicationListItem",
|
||||
"UnreadPublicationItem",
|
||||
"resolve_publication_view_mode",
|
||||
"get_latest_run_id_for_user",
|
||||
"publications_query",
|
||||
"get_publication_item_for_user",
|
||||
"list_for_user",
|
||||
"list_unread_for_user",
|
||||
"retry_pdf_for_user",
|
||||
"hydrate_pdf_enrichment_state",
|
||||
"schedule_retry_pdf_enrichment_for_row",
|
||||
"list_pdf_queue_items",
|
||||
"list_pdf_queue_page",
|
||||
"count_favorite_for_user",
|
||||
"count_for_user",
|
||||
"count_latest_for_user",
|
||||
"count_pdf_queue_items",
|
||||
"count_unread_for_user",
|
||||
"enqueue_all_missing_pdf_jobs",
|
||||
"enqueue_retry_pdf_job_for_publication_id",
|
||||
"schedule_missing_pdf_enrichment_for_user",
|
||||
"count_for_user",
|
||||
"count_favorite_for_user",
|
||||
"count_unread_for_user",
|
||||
"count_latest_for_user",
|
||||
"get_latest_run_id_for_user",
|
||||
"get_publication_item_for_user",
|
||||
"hydrate_pdf_enrichment_state",
|
||||
"list_for_user",
|
||||
"list_pdf_queue_items",
|
||||
"list_pdf_queue_page",
|
||||
"list_unread_for_user",
|
||||
"mark_all_unread_as_read_for_user",
|
||||
"mark_selected_as_read_for_user",
|
||||
"publications_query",
|
||||
"resolve_publication_view_mode",
|
||||
"retry_pdf_for_user",
|
||||
"schedule_missing_pdf_enrichment_for_user",
|
||||
"schedule_retry_pdf_enrichment_for_row",
|
||||
"set_publication_favorite_for_user",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Iterable
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy import delete, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -113,9 +113,7 @@ async def _load_publication(
|
|||
*,
|
||||
publication_id: int,
|
||||
) -> Publication | None:
|
||||
result = await db_session.execute(
|
||||
select(Publication).where(Publication.id == publication_id)
|
||||
)
|
||||
result = await db_session.execute(select(Publication).where(Publication.id == publication_id))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
|
|
@ -160,9 +158,7 @@ async def _migrate_scholar_links(
|
|||
dup_links = dup_links_result.scalars().all()
|
||||
|
||||
winner_profiles_result = await db_session.execute(
|
||||
select(ScholarPublication.scholar_profile_id).where(
|
||||
ScholarPublication.publication_id == winner_id
|
||||
)
|
||||
select(ScholarPublication.scholar_profile_id).where(ScholarPublication.publication_id == winner_id)
|
||||
)
|
||||
winner_profiles: set[int] = {row for (row,) in winner_profiles_result}
|
||||
|
||||
|
|
@ -346,11 +342,7 @@ def _candidate_from_row(
|
|||
|
||||
|
||||
def _normalized_tokens(tokens: Iterable[str]) -> set[str]:
|
||||
return {
|
||||
token
|
||||
for token in tokens
|
||||
if len(token) >= NEAR_DUP_MIN_TOKEN_LENGTH and token not in NEAR_DUP_STOPWORDS
|
||||
}
|
||||
return {token for token in tokens if len(token) >= NEAR_DUP_MIN_TOKEN_LENGTH and token not in NEAR_DUP_STOPWORDS}
|
||||
|
||||
|
||||
def _cluster_candidate_groups(
|
||||
|
|
|
|||
|
|
@ -4,12 +4,12 @@ import logging
|
|||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.logging_utils import structured_log
|
||||
from app.services.domains.publications.pdf_queue import (
|
||||
enqueue_missing_pdf_jobs,
|
||||
enqueue_retry_pdf_job,
|
||||
overlay_pdf_job_state,
|
||||
)
|
||||
from app.logging_utils import structured_log
|
||||
from app.services.domains.publications.types import PublicationListItem
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -30,7 +30,9 @@ async def schedule_missing_pdf_enrichment_for_user(
|
|||
rows=items,
|
||||
max_items=max_items,
|
||||
)
|
||||
structured_log(logger, "info", "publications.enrichment.scheduled", user_id=user_id, publication_count=len(queued_ids))
|
||||
structured_log(
|
||||
logger, "info", "publications.enrichment.scheduled", user_id=user_id, publication_count=len(queued_ids)
|
||||
)
|
||||
return len(queued_ids)
|
||||
|
||||
|
||||
|
|
@ -47,7 +49,14 @@ async def schedule_retry_pdf_enrichment_for_row(
|
|||
request_email=request_email,
|
||||
row=item,
|
||||
)
|
||||
structured_log(logger, "info", "publications.enrichment.retry_scheduled", user_id=user_id, publication_id=item.publication_id, queued=queued)
|
||||
structured_log(
|
||||
logger,
|
||||
"info",
|
||||
"publications.enrichment.retry_scheduled",
|
||||
user_id=user_id,
|
||||
publication_id=item.publication_id,
|
||||
queued=queued,
|
||||
)
|
||||
return queued
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -51,10 +51,7 @@ async def list_for_user(
|
|||
snapshot_before=snapshot_before,
|
||||
)
|
||||
)
|
||||
rows = [
|
||||
publication_list_item_from_row(row, latest_run_id=latest_run_id)
|
||||
for row in result.all()
|
||||
]
|
||||
rows = [publication_list_item_from_row(row, latest_run_id=latest_run_id) for row in result.all()]
|
||||
return await identifier_service.overlay_publication_items_with_display_identifiers(
|
||||
db_session,
|
||||
items=rows,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from sqlalchemy import Select, and_, func, literal, or_, select, union_all
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -17,6 +17,7 @@ from app.db.models import (
|
|||
User,
|
||||
)
|
||||
from app.db.session import get_session_factory
|
||||
from app.logging_utils import structured_log
|
||||
from app.services.domains.publication_identifiers import application as identifier_service
|
||||
from app.services.domains.publication_identifiers.types import DisplayIdentifier
|
||||
from app.services.domains.publications.pdf_resolution_pipeline import (
|
||||
|
|
@ -27,7 +28,6 @@ from app.services.domains.unpaywall.application import (
|
|||
FAILURE_RESOLUTION_EXCEPTION,
|
||||
OaResolutionOutcome,
|
||||
)
|
||||
from app.logging_utils import structured_log
|
||||
from app.settings import settings
|
||||
|
||||
PDF_STATUS_UNTRACKED = "untracked"
|
||||
|
|
@ -85,7 +85,7 @@ class PdfQueuePage:
|
|||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
return datetime.now(UTC)
|
||||
|
||||
|
||||
def _publication_ids(rows: list[PublicationListItem]) -> list[int]:
|
||||
|
|
@ -472,7 +472,13 @@ async def _resolve_publication_row(
|
|||
# Propagate upward so the batch loop can stop immediately.
|
||||
raise
|
||||
except Exception as exc: # pragma: no cover - defensive network boundary
|
||||
structured_log(logger, "warning", "publications.pdf_queue.resolve_failed", publication_id=row.publication_id, error=str(exc))
|
||||
structured_log(
|
||||
logger,
|
||||
"warning",
|
||||
"publications.pdf_queue.resolve_failed",
|
||||
publication_id=row.publication_id,
|
||||
error=str(exc),
|
||||
)
|
||||
outcome = _failed_outcome(row=row)
|
||||
arxiv_rate_limited = False
|
||||
await _persist_outcome(
|
||||
|
|
@ -514,9 +520,19 @@ async def _run_resolution_task(
|
|||
)
|
||||
if arxiv_rate_limited and arxiv_lookup_allowed:
|
||||
arxiv_lookup_allowed = False
|
||||
structured_log(logger, "warning", "pdf_queue.arxiv_batch_disabled", detail="arXiv temporarily disabled for remaining batch after rate limit")
|
||||
structured_log(
|
||||
logger,
|
||||
"warning",
|
||||
"pdf_queue.arxiv_batch_disabled",
|
||||
detail="arXiv temporarily disabled for remaining batch after rate limit",
|
||||
)
|
||||
except OpenAlexBudgetExhaustedError:
|
||||
structured_log(logger, "warning", "pdf_queue.budget_exhausted", detail="Stopping PDF resolution batch — OpenAlex daily budget exhausted")
|
||||
structured_log(
|
||||
logger,
|
||||
"warning",
|
||||
"pdf_queue.budget_exhausted",
|
||||
detail="Stopping PDF resolution batch — OpenAlex daily budget exhausted",
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
|
|
@ -679,7 +695,7 @@ async def _missing_pdf_candidates(
|
|||
limit: int,
|
||||
) -> list[PublicationListItem]:
|
||||
bounded_limit = max(1, min(int(limit), 5000))
|
||||
now = datetime.now(timezone.utc)
|
||||
now = datetime.now(UTC)
|
||||
cooldown_threshold = now - timedelta(days=7)
|
||||
|
||||
result = await db_session.execute(
|
||||
|
|
@ -701,10 +717,7 @@ async def _missing_pdf_candidates(
|
|||
.order_by(Publication.updated_at.desc(), Publication.id.desc())
|
||||
.limit(bounded_limit)
|
||||
)
|
||||
return [
|
||||
_queue_candidate_from_publication(publication)
|
||||
for publication in result.scalars()
|
||||
]
|
||||
return [_queue_candidate_from_publication(publication) for publication in result.scalars()]
|
||||
|
||||
|
||||
async def enqueue_all_missing_pdf_jobs(
|
||||
|
|
@ -902,9 +915,7 @@ async def count_pdf_queue_items(
|
|||
if normalized_status == PDF_STATUS_UNTRACKED:
|
||||
result = await db_session.execute(_untracked_queue_count_select())
|
||||
return int(result.scalar_one() or 0)
|
||||
tracked_result = await db_session.execute(
|
||||
_tracked_queue_count_select(status=normalized_status)
|
||||
)
|
||||
tracked_result = await db_session.execute(_tracked_queue_count_select(status=normalized_status))
|
||||
tracked_count = int(tracked_result.scalar_one() or 0)
|
||||
if normalized_status is not None:
|
||||
return tracked_count
|
||||
|
|
@ -946,9 +957,7 @@ async def drain_ready_jobs(
|
|||
limit: int,
|
||||
max_attempts: int,
|
||||
) -> int:
|
||||
result = await db_session.execute(
|
||||
select(User.id).where(User.is_active.is_(True)).order_by(User.id.asc()).limit(1)
|
||||
)
|
||||
result = await db_session.execute(select(User.id).where(User.is_active.is_(True)).order_by(User.id.asc()).limit(1))
|
||||
system_user_id = result.scalar_one_or_none()
|
||||
if system_user_id is None:
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from app.logging_utils import structured_log
|
||||
from app.services.domains.arxiv.application import ArxivRateLimitError
|
||||
from app.services.domains.arxiv.guards import arxiv_skip_reason_for_item
|
||||
from app.services.domains.openalex.client import OpenAlexBudgetExhaustedError
|
||||
from app.services.domains.publications.types import PublicationListItem
|
||||
from app.services.domains.unpaywall.application import OaResolutionOutcome, resolve_publication_oa_outcomes
|
||||
from app.logging_utils import structured_log
|
||||
from app.settings import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -66,6 +66,7 @@ async def _openalex_outcome(
|
|||
return None
|
||||
|
||||
import re
|
||||
|
||||
safe_title = re.sub(r"[^\w\s]", " ", row.title)
|
||||
safe_title = " ".join(safe_title.split())
|
||||
if not safe_title:
|
||||
|
|
@ -107,12 +108,24 @@ async def _arxiv_outcome(
|
|||
from app.services.domains.arxiv.application import discover_arxiv_id_for_publication
|
||||
|
||||
if not allow_lookup:
|
||||
structured_log(logger, "info", "pdf_resolution.arxiv_skipped", publication_id=int(row.publication_id), skip_reason="batch_arxiv_cooldown_active")
|
||||
structured_log(
|
||||
logger,
|
||||
"info",
|
||||
"pdf_resolution.arxiv_skipped",
|
||||
publication_id=int(row.publication_id),
|
||||
skip_reason="batch_arxiv_cooldown_active",
|
||||
)
|
||||
return None
|
||||
|
||||
skip_reason = arxiv_skip_reason_for_item(item=row)
|
||||
if skip_reason is not None:
|
||||
structured_log(logger, "info", "pdf_resolution.arxiv_skipped", publication_id=int(row.publication_id), skip_reason=skip_reason)
|
||||
structured_log(
|
||||
logger,
|
||||
"info",
|
||||
"pdf_resolution.arxiv_skipped",
|
||||
publication_id=int(row.publication_id),
|
||||
skip_reason=skip_reason,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
|
|
@ -134,7 +147,6 @@ async def _arxiv_outcome(
|
|||
return None
|
||||
|
||||
|
||||
|
||||
async def _oa_outcome(
|
||||
*,
|
||||
row: PublicationListItem,
|
||||
|
|
|
|||
|
|
@ -228,9 +228,7 @@ def publication_list_item_from_row(
|
|||
is_read=bool(is_read),
|
||||
is_favorite=bool(is_favorite),
|
||||
first_seen_at=created_at,
|
||||
is_new_in_latest_run=(
|
||||
latest_run_id is not None and int(first_seen_run_id or 0) == latest_run_id
|
||||
),
|
||||
is_new_in_latest_run=(latest_run_id is not None and int(first_seen_run_id or 0) == latest_run_id),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -17,11 +17,7 @@ def _normalized_selection_pairs(selections: list[tuple[int, int]]) -> set[tuple[
|
|||
|
||||
|
||||
def _scoped_scholar_ids_query(*, user_id: int):
|
||||
return (
|
||||
select(ScholarProfile.id)
|
||||
.where(ScholarProfile.user_id == user_id)
|
||||
.scalar_subquery()
|
||||
)
|
||||
return select(ScholarProfile.id).where(ScholarProfile.user_id == user_id).scalar_subquery()
|
||||
|
||||
|
||||
async def mark_all_unread_as_read_for_user(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue