From 74249fa24c5c427135d2ff0e569097ae1d53854b Mon Sep 17 00:00:00 2001 From: Justin Visser Date: Fri, 27 Feb 2026 15:49:27 +0100 Subject: [PATCH] fix: resolve all mypy type errors across service modules Co-Authored-By: Claude Opus 4.6 --- app/services/arxiv/client.py | 2 +- app/services/dbops/application.py | 10 +++++----- app/services/ingestion/scheduler.py | 7 ++++--- app/services/ingestion/scholar_processing.py | 2 +- app/services/portability/exporting.py | 2 +- app/services/publications/pdf_queue_queries.py | 7 ++++--- app/services/publications/queries.py | 9 +++++---- app/services/publications/read_state.py | 10 ++++++---- app/services/runs/queue_queries.py | 4 +++- app/services/scholars/author_search_cache.py | 17 +++++++++-------- 10 files changed, 39 insertions(+), 31 deletions(-) diff --git a/app/services/arxiv/client.py b/app/services/arxiv/client.py index 27f048f..41de63d 100644 --- a/app/services/arxiv/client.py +++ b/app/services/arxiv/client.py @@ -232,7 +232,7 @@ async def _request_arxiv_feed( timeout_value = _timeout_seconds(timeout_seconds) headers = {"User-Agent": f"scholar-scraper/1.0 (mailto:{_contact_email(request_email)})"} async with httpx.AsyncClient(timeout=timeout_value, follow_redirects=True, headers=headers) as client: - return await client.get(_ARXIV_API_URL, params=params) + return await client.get(_ARXIV_API_URL, params=params) # type: ignore[arg-type] return await run_with_global_arxiv_limit( fetch=_fetch, diff --git a/app/services/dbops/application.py b/app/services/dbops/application.py index 7c59033..a5fa077 100644 --- a/app/services/dbops/application.py +++ b/app/services/dbops/application.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import UTC, datetime from typing import Any -from sqlalchemy import delete, exists, func, select, update +from sqlalchemy import CursorResult, delete, exists, func, select, update from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import DataRepairJob, IngestionQueueItem, Publication, ScholarProfile, ScholarPublication @@ -152,7 +152,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( + result: CursorResult[Any] = await db_session.execute( # type: ignore[assignment] delete(ScholarPublication).where(ScholarPublication.scholar_profile_id.in_(target_scholar_profile_ids)) ) return int(result.rowcount or 0) @@ -167,7 +167,7 @@ async def _delete_queue_for_targets( 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) + result: CursorResult[Any] = await db_session.execute(stmt) # type: ignore[assignment] return int(result.rowcount or 0) @@ -180,7 +180,7 @@ async def _reset_scholar_tracking_state( stmt = update(ScholarProfile).where(ScholarProfile.id.in_(target_scholar_profile_ids)) if user_id is not None: stmt = stmt.where(ScholarProfile.user_id == user_id) - result = await db_session.execute( + result: CursorResult[Any] = await db_session.execute( # type: ignore[assignment] stmt.values( baseline_completed=False, last_initial_page_fingerprint_sha256=None, @@ -193,7 +193,7 @@ async def _reset_scholar_tracking_state( async def _delete_orphan_publications(db_session: AsyncSession) -> int: - result = await db_session.execute( + result: CursorResult[Any] = await db_session.execute( # type: ignore[assignment] delete(Publication).where(~exists(select(1).where(ScholarPublication.publication_id == Publication.id))) ) return int(result.rowcount or 0) diff --git a/app/services/ingestion/scheduler.py b/app/services/ingestion/scheduler.py index 975cb53..23b70d0 100644 --- a/app/services/ingestion/scheduler.py +++ b/app/services/ingestion/scheduler.py @@ -4,6 +4,7 @@ import asyncio import logging from dataclasses import dataclass from datetime import UTC, datetime, timedelta +from typing import Any from sqlalchemy import select @@ -158,7 +159,7 @@ class SchedulerService: continue await self._run_candidate(candidate) - async def _load_candidate_rows(self) -> list[tuple]: + async def _load_candidate_rows(self) -> list[Any]: session_factory = get_session_factory() async with session_factory() as session: result = await session.execute( @@ -173,10 +174,10 @@ class SchedulerService: .where(User.is_active.is_(True), UserSetting.auto_run_enabled.is_(True)) .order_by(UserSetting.user_id.asc()) ) - return result.all() + return list(result.all()) @staticmethod - def _candidate_from_row(row: tuple, *, now_utc: datetime) -> _AutoRunCandidate | None: + def _candidate_from_row(row: Any, *, now_utc: datetime) -> _AutoRunCandidate | None: user_id, run_interval_minutes, request_delay_seconds, cooldown_until, cooldown_reason = row if cooldown_until is not None and cooldown_until.tzinfo is None: cooldown_until = cooldown_until.replace(tzinfo=UTC) diff --git a/app/services/ingestion/scholar_processing.py b/app/services/ingestion/scholar_processing.py index 393f85c..3096dee 100644 --- a/app/services/ingestion/scholar_processing.py +++ b/app/services/ingestion/scholar_processing.py @@ -547,7 +547,7 @@ async def run_scholar_iteration( queue_delay_seconds: int, ) -> RunProgress: progress = RunProgress() - scholar_kwargs = { + scholar_kwargs: dict[str, Any] = { "request_delay_seconds": request_delay_seconds, "network_error_retries": network_error_retries, "retry_backoff_seconds": retry_backoff_seconds, diff --git a/app/services/portability/exporting.py b/app/services/portability/exporting.py index cd88cc6..0589498 100644 --- a/app/services/portability/exporting.py +++ b/app/services/portability/exporting.py @@ -23,7 +23,7 @@ def _serialize_export_scholar(profile: ScholarProfile) -> dict[str, Any]: } -def _serialize_export_publication(row: tuple[Any, ...]) -> dict[str, Any]: +def _serialize_export_publication(row: Any) -> dict[str, Any]: ( scholar_id, cluster_id, diff --git a/app/services/publications/pdf_queue_queries.py b/app/services/publications/pdf_queue_queries.py index d8dad1f..a0ee0b2 100644 --- a/app/services/publications/pdf_queue_queries.py +++ b/app/services/publications/pdf_queue_queries.py @@ -2,6 +2,7 @@ from __future__ import annotations from dataclasses import dataclass from datetime import UTC, datetime, timedelta +from typing import Any from sqlalchemy import Select, and_, func, literal, or_, select, union_all from sqlalchemy.ext.asyncio import AsyncSession @@ -43,7 +44,7 @@ def _retry_item_label(display_name: str | None, scholar_id: str | None) -> str: def _retry_item_from_publication( publication: Publication, *, - link_row: tuple | None, + link_row: Any | None, ) -> PublicationListItem: if link_row is None: scholar_profile_id = 0 @@ -270,7 +271,7 @@ class PdfQueueListItem: display_identifier: DisplayIdentifier | None = None -def _queue_item_from_row(row: tuple) -> PdfQueueListItem: +def _queue_item_from_row(row: Any) -> PdfQueueListItem: return PdfQueueListItem( publication_id=int(row[0]), title=str(row[1] or ""), @@ -292,7 +293,7 @@ def _queue_item_from_row(row: tuple) -> PdfQueueListItem: async def _hydrated_queue_items( db_session: AsyncSession, *, - rows: list[tuple], + rows: list[Any], ) -> list[PdfQueueListItem]: items = [_queue_item_from_row(row) for row in rows] return await identifier_service.overlay_pdf_queue_items_with_display_identifiers( diff --git a/app/services/publications/queries.py b/app/services/publications/queries.py index fb03b61..2de11d6 100644 --- a/app/services/publications/queries.py +++ b/app/services/publications/queries.py @@ -1,8 +1,9 @@ from __future__ import annotations from datetime import datetime +from typing import Any -from sqlalchemy import Select, case, func, select +from sqlalchemy import Select, case, false as sa_false, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import ( @@ -124,7 +125,7 @@ def publications_query( stmt = stmt.where(ScholarPublication.is_read.is_(False)) if mode == MODE_LATEST: if latest_run_id is None: - return stmt.where(False) + return stmt.where(sa_false()) stmt = stmt.where(ScholarPublication.first_seen_run_id == latest_run_id) if snapshot_before is not None: stmt = stmt.where(ScholarPublication.created_at <= snapshot_before) @@ -195,7 +196,7 @@ async def get_publication_item_for_user( def publication_list_item_from_row( - row: tuple, + row: Any, *, latest_run_id: int | None, ) -> PublicationListItem: @@ -232,7 +233,7 @@ def publication_list_item_from_row( ) -def unread_item_from_row(row: tuple) -> UnreadPublicationItem: +def unread_item_from_row(row: Any) -> UnreadPublicationItem: ( publication_id, scholar_profile_id, diff --git a/app/services/publications/read_state.py b/app/services/publications/read_state.py index b816d59..2a56b61 100644 --- a/app/services/publications/read_state.py +++ b/app/services/publications/read_state.py @@ -1,6 +1,8 @@ from __future__ import annotations -from sqlalchemy import select, tuple_, update +from typing import Any + +from sqlalchemy import CursorResult, select, tuple_, update from sqlalchemy.ext.asyncio import AsyncSession from app.db.models import ScholarProfile, ScholarPublication @@ -34,7 +36,7 @@ async def mark_all_unread_as_read_for_user( ) .values(is_read=True) ) - result = await db_session.execute(stmt) + result: CursorResult[Any] = await db_session.execute(stmt) # type: ignore[assignment] await db_session.commit() return int(result.rowcount or 0) @@ -62,7 +64,7 @@ async def mark_selected_as_read_for_user( ) .values(is_read=True) ) - result = await db_session.execute(stmt) + result: CursorResult[Any] = await db_session.execute(stmt) # type: ignore[assignment] await db_session.commit() return int(result.rowcount or 0) @@ -85,6 +87,6 @@ async def set_publication_favorite_for_user( ) .values(is_favorite=bool(is_favorite)) ) - result = await db_session.execute(stmt) + result: CursorResult[Any] = await db_session.execute(stmt) # type: ignore[assignment] await db_session.commit() return int(result.rowcount or 0) diff --git a/app/services/runs/queue_queries.py b/app/services/runs/queue_queries.py index 588a47e..2d1127a 100644 --- a/app/services/runs/queue_queries.py +++ b/app/services/runs/queue_queries.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + from sqlalchemy import and_, select from app.db.models import IngestionQueueItem, ScholarProfile @@ -38,7 +40,7 @@ def queue_item_select(*, user_id: int): ) -def queue_list_item_from_row(row: tuple) -> QueueListItem: +def queue_list_item_from_row(row: Any) -> QueueListItem: ( item_id, scholar_profile_id, diff --git a/app/services/scholars/author_search_cache.py b/app/services/scholars/author_search_cache.py index 01be986..a0bab2f 100644 --- a/app/services/scholars/author_search_cache.py +++ b/app/services/scholars/author_search_cache.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import UTC, datetime, timedelta +from typing import cast from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -136,14 +137,14 @@ def _deserialize_parsed_author_search_page(payload: object) -> ParsedAuthorSearc state_reason=str(payload.get("state_reason", "")).strip() or "unknown", candidates=[ ScholarSearchCandidate( - scholar_id=item["scholar_id"], - display_name=item["display_name"], - affiliation=item["affiliation"], - email_domain=item["email_domain"], - cited_by_count=item["cited_by_count"], - interests=item["interests"], - profile_url=item["profile_url"], - profile_image_url=item["profile_image_url"], + scholar_id=cast(str, item["scholar_id"]), + display_name=cast(str, item["display_name"]), + affiliation=cast("str | None", item["affiliation"]), + email_domain=cast("str | None", item["email_domain"]), + cited_by_count=cast("int | None", item["cited_by_count"]), + interests=cast("list[str]", item["interests"]), + profile_url=cast(str, item["profile_url"]), + profile_image_url=cast("str | None", item["profile_image_url"]), ) for item in normalized_candidates ],