refactor: extract helpers from remaining long functions
- Extract _start_manual_run and _spawn_background_execution from run_manual route - Extract _upsert_page_publications to deduplicate pagination loop - Extract _classify_attempt from fetch_and_parse_with_retry - Extract _log_alert_threshold_warnings from complete_run_for_user - Functions 3f-3j accepted as kwargs-exception (body ≤50 lines excluding signatures) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2a6e573a52
commit
72bc046215
4 changed files with 156 additions and 87 deletions
|
|
@ -176,6 +176,67 @@ async def cancel_run(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _start_manual_run(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
*,
|
||||||
|
current_user: User,
|
||||||
|
ingest_service: ingestion_service.ScholarIngestionService,
|
||||||
|
idempotency_key: str | None,
|
||||||
|
) -> tuple[Any, Any, list, dict]:
|
||||||
|
user_settings = await user_settings_service.get_or_create_settings(db_session, user_id=current_user.id)
|
||||||
|
run, scholars, start_cstart_map = await ingest_service.initialize_run(
|
||||||
|
db_session,
|
||||||
|
user_id=current_user.id,
|
||||||
|
trigger_type=RunTriggerType.MANUAL,
|
||||||
|
request_delay_seconds=user_settings.request_delay_seconds,
|
||||||
|
network_error_retries=settings.ingestion_network_error_retries,
|
||||||
|
retry_backoff_seconds=settings.ingestion_retry_backoff_seconds,
|
||||||
|
max_pages_per_scholar=settings.ingestion_max_pages_per_scholar,
|
||||||
|
page_size=settings.ingestion_page_size,
|
||||||
|
idempotency_key=idempotency_key,
|
||||||
|
alert_blocked_failure_threshold=settings.ingestion_alert_blocked_failure_threshold,
|
||||||
|
alert_network_failure_threshold=settings.ingestion_alert_network_failure_threshold,
|
||||||
|
alert_retry_scheduled_threshold=settings.ingestion_alert_retry_scheduled_threshold,
|
||||||
|
)
|
||||||
|
return user_settings, run, scholars, start_cstart_map
|
||||||
|
|
||||||
|
|
||||||
|
def _spawn_background_execution(
|
||||||
|
ingest_service: ingestion_service.ScholarIngestionService,
|
||||||
|
*,
|
||||||
|
run: Any,
|
||||||
|
current_user: User,
|
||||||
|
scholars: list,
|
||||||
|
start_cstart_map: dict,
|
||||||
|
user_settings: Any,
|
||||||
|
idempotency_key: str | None,
|
||||||
|
) -> None:
|
||||||
|
from app.db.session import get_session_factory
|
||||||
|
|
||||||
|
task = asyncio.create_task(
|
||||||
|
ingest_service.execute_run(
|
||||||
|
session_factory=get_session_factory(),
|
||||||
|
run_id=run.id,
|
||||||
|
user_id=current_user.id,
|
||||||
|
scholars=scholars,
|
||||||
|
start_cstart_map=start_cstart_map,
|
||||||
|
request_delay_seconds=user_settings.request_delay_seconds,
|
||||||
|
network_error_retries=settings.ingestion_network_error_retries,
|
||||||
|
retry_backoff_seconds=settings.ingestion_retry_backoff_seconds,
|
||||||
|
max_pages_per_scholar=settings.ingestion_max_pages_per_scholar,
|
||||||
|
page_size=settings.ingestion_page_size,
|
||||||
|
auto_queue_continuations=settings.ingestion_continuation_queue_enabled,
|
||||||
|
queue_delay_seconds=settings.ingestion_continuation_base_delay_seconds,
|
||||||
|
alert_blocked_failure_threshold=settings.ingestion_alert_blocked_failure_threshold,
|
||||||
|
alert_network_failure_threshold=settings.ingestion_alert_network_failure_threshold,
|
||||||
|
alert_retry_scheduled_threshold=settings.ingestion_alert_retry_scheduled_threshold,
|
||||||
|
idempotency_key=idempotency_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_background_tasks.add(task)
|
||||||
|
task.add_done_callback(_background_tasks.discard)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/manual",
|
"/manual",
|
||||||
response_model=ManualRunEnvelope,
|
response_model=ManualRunEnvelope,
|
||||||
|
|
@ -202,52 +263,22 @@ async def run_manual(
|
||||||
return reused_payload
|
return reused_payload
|
||||||
|
|
||||||
try:
|
try:
|
||||||
user_settings = await user_settings_service.get_or_create_settings(db_session, user_id=current_user.id)
|
user_settings, run, scholars, start_cstart_map = await _start_manual_run(
|
||||||
|
|
||||||
# Initialize run (creates the record and performs safety checks)
|
|
||||||
run, scholars, start_cstart_map = await ingest_service.initialize_run(
|
|
||||||
db_session,
|
db_session,
|
||||||
user_id=current_user.id,
|
current_user=current_user,
|
||||||
trigger_type=RunTriggerType.MANUAL,
|
ingest_service=ingest_service,
|
||||||
request_delay_seconds=user_settings.request_delay_seconds,
|
|
||||||
network_error_retries=settings.ingestion_network_error_retries,
|
|
||||||
retry_backoff_seconds=settings.ingestion_retry_backoff_seconds,
|
|
||||||
max_pages_per_scholar=settings.ingestion_max_pages_per_scholar,
|
|
||||||
page_size=settings.ingestion_page_size,
|
|
||||||
idempotency_key=idempotency_key,
|
idempotency_key=idempotency_key,
|
||||||
alert_blocked_failure_threshold=settings.ingestion_alert_blocked_failure_threshold,
|
|
||||||
alert_network_failure_threshold=settings.ingestion_alert_network_failure_threshold,
|
|
||||||
alert_retry_scheduled_threshold=settings.ingestion_alert_retry_scheduled_threshold,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await db_session.commit()
|
await db_session.commit()
|
||||||
|
_spawn_background_execution(
|
||||||
# Kick off background execution
|
ingest_service,
|
||||||
from app.db.session import get_session_factory
|
run=run,
|
||||||
|
current_user=current_user,
|
||||||
task = asyncio.create_task(
|
scholars=scholars,
|
||||||
ingest_service.execute_run(
|
start_cstart_map=start_cstart_map,
|
||||||
session_factory=get_session_factory(),
|
user_settings=user_settings,
|
||||||
run_id=run.id,
|
idempotency_key=idempotency_key,
|
||||||
user_id=current_user.id,
|
|
||||||
scholars=scholars,
|
|
||||||
start_cstart_map=start_cstart_map,
|
|
||||||
request_delay_seconds=user_settings.request_delay_seconds,
|
|
||||||
network_error_retries=settings.ingestion_network_error_retries,
|
|
||||||
retry_backoff_seconds=settings.ingestion_retry_backoff_seconds,
|
|
||||||
max_pages_per_scholar=settings.ingestion_max_pages_per_scholar,
|
|
||||||
page_size=settings.ingestion_page_size,
|
|
||||||
auto_queue_continuations=settings.ingestion_continuation_queue_enabled,
|
|
||||||
queue_delay_seconds=settings.ingestion_continuation_base_delay_seconds,
|
|
||||||
alert_blocked_failure_threshold=settings.ingestion_alert_blocked_failure_threshold,
|
|
||||||
alert_network_failure_threshold=settings.ingestion_alert_network_failure_threshold,
|
|
||||||
alert_retry_scheduled_threshold=settings.ingestion_alert_retry_scheduled_threshold,
|
|
||||||
idempotency_key=idempotency_key,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
_background_tasks.add(task)
|
|
||||||
task.add_done_callback(_background_tasks.discard)
|
|
||||||
|
|
||||||
return success_payload(
|
return success_payload(
|
||||||
request,
|
request,
|
||||||
data={
|
data={
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,24 @@ class PageFetcher:
|
||||||
if sleep_seconds > 0:
|
if sleep_seconds > 0:
|
||||||
await asyncio.sleep(sleep_seconds)
|
await asyncio.sleep(sleep_seconds)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _classify_attempt(
|
||||||
|
parsed_page: ParsedProfilePage,
|
||||||
|
*,
|
||||||
|
network_attempts: int,
|
||||||
|
rate_limit_attempts: int,
|
||||||
|
) -> tuple[int, int, int]:
|
||||||
|
if parsed_page.state == ParseState.NETWORK_ERROR:
|
||||||
|
network_attempts += 1
|
||||||
|
return network_attempts, rate_limit_attempts, network_attempts
|
||||||
|
if (
|
||||||
|
parsed_page.state == ParseState.BLOCKED_OR_CAPTCHA
|
||||||
|
and parsed_page.state_reason == "blocked_http_429_rate_limited"
|
||||||
|
):
|
||||||
|
rate_limit_attempts += 1
|
||||||
|
return network_attempts, rate_limit_attempts, rate_limit_attempts
|
||||||
|
return network_attempts, rate_limit_attempts, network_attempts + rate_limit_attempts + 1
|
||||||
|
|
||||||
async def fetch_and_parse_with_retry(
|
async def fetch_and_parse_with_retry(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|
@ -169,19 +187,9 @@ class PageFetcher:
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
)
|
)
|
||||||
parsed_page = self.parse_page_or_layout_error(fetch_result=fetch_result)
|
parsed_page = self.parse_page_or_layout_error(fetch_result=fetch_result)
|
||||||
|
network_attempts, rate_limit_attempts, total_attempts = self._classify_attempt(
|
||||||
if parsed_page.state == ParseState.NETWORK_ERROR:
|
parsed_page, network_attempts=network_attempts, rate_limit_attempts=rate_limit_attempts
|
||||||
network_attempts += 1
|
)
|
||||||
total_attempts = network_attempts
|
|
||||||
elif (
|
|
||||||
parsed_page.state == ParseState.BLOCKED_OR_CAPTCHA
|
|
||||||
and parsed_page.state_reason == "blocked_http_429_rate_limited"
|
|
||||||
):
|
|
||||||
rate_limit_attempts += 1
|
|
||||||
total_attempts = rate_limit_attempts
|
|
||||||
else:
|
|
||||||
total_attempts = network_attempts + rate_limit_attempts + 1
|
|
||||||
|
|
||||||
attempt_log.append(
|
attempt_log.append(
|
||||||
{
|
{
|
||||||
"attempt": total_attempts,
|
"attempt": total_attempts,
|
||||||
|
|
@ -192,7 +200,6 @@ class PageFetcher:
|
||||||
"fetch_error": fetch_result.error,
|
"fetch_error": fetch_result.error,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self._should_retry(
|
if not self._should_retry(
|
||||||
parsed_page=parsed_page,
|
parsed_page=parsed_page,
|
||||||
network_attempt_count=network_attempts,
|
network_attempt_count=network_attempts,
|
||||||
|
|
@ -201,7 +208,6 @@ class PageFetcher:
|
||||||
rate_limit_retries=rate_limit_retries,
|
rate_limit_retries=rate_limit_retries,
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
|
|
||||||
await self._sleep_backoff(
|
await self._sleep_backoff(
|
||||||
scholar_id=scholar_id,
|
scholar_id=scholar_id,
|
||||||
cstart=cstart,
|
cstart=cstart,
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,24 @@ class PaginationEngine:
|
||||||
|
|
||||||
# ── Pagination loop ──────────────────────────────────────────────
|
# ── Pagination loop ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def _upsert_page_publications(
|
||||||
|
db_session: Any,
|
||||||
|
*,
|
||||||
|
run: CrawlRun,
|
||||||
|
scholar: ScholarProfile,
|
||||||
|
publications: list,
|
||||||
|
seen_canonical: set[str],
|
||||||
|
state: PagedLoopState,
|
||||||
|
upsert_publications_fn: Any,
|
||||||
|
) -> None:
|
||||||
|
deduped = _dedupe_publication_candidates(list(publications), seen_canonical=seen_canonical)
|
||||||
|
if deduped:
|
||||||
|
discovered_count = await upsert_publications_fn(
|
||||||
|
db_session, run=run, scholar=scholar, publications=deduped
|
||||||
|
)
|
||||||
|
state.discovered_publication_count += discovered_count
|
||||||
|
|
||||||
async def _paginate_loop(
|
async def _paginate_loop(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|
@ -292,14 +310,15 @@ class PaginationEngine:
|
||||||
seen_canonical: set[str] = set()
|
seen_canonical: set[str] = set()
|
||||||
|
|
||||||
if state.parsed_page.publications:
|
if state.parsed_page.publications:
|
||||||
deduped_first = _dedupe_publication_candidates(
|
await self._upsert_page_publications(
|
||||||
list(state.parsed_page.publications), seen_canonical=seen_canonical
|
db_session,
|
||||||
|
run=run,
|
||||||
|
scholar=scholar,
|
||||||
|
publications=state.parsed_page.publications,
|
||||||
|
seen_canonical=seen_canonical,
|
||||||
|
state=state,
|
||||||
|
upsert_publications_fn=upsert_publications_fn,
|
||||||
)
|
)
|
||||||
if deduped_first:
|
|
||||||
discovered_count = await upsert_publications_fn(
|
|
||||||
db_session, run=run, scholar=scholar, publications=deduped_first
|
|
||||||
)
|
|
||||||
state.discovered_publication_count += discovered_count
|
|
||||||
|
|
||||||
while state.parsed_page.has_show_more_button:
|
while state.parsed_page.has_show_more_button:
|
||||||
await db_session.refresh(run)
|
await db_session.refresh(run)
|
||||||
|
|
@ -337,14 +356,15 @@ class PaginationEngine:
|
||||||
)
|
)
|
||||||
|
|
||||||
if next_parsed_page.publications:
|
if next_parsed_page.publications:
|
||||||
deduped_next = _dedupe_publication_candidates(
|
await self._upsert_page_publications(
|
||||||
list(next_parsed_page.publications), seen_canonical=seen_canonical
|
db_session,
|
||||||
|
run=run,
|
||||||
|
scholar=scholar,
|
||||||
|
publications=next_parsed_page.publications,
|
||||||
|
seen_canonical=seen_canonical,
|
||||||
|
state=state,
|
||||||
|
upsert_publications_fn=upsert_publications_fn,
|
||||||
)
|
)
|
||||||
if deduped_next:
|
|
||||||
discovered_count = await upsert_publications_fn(
|
|
||||||
db_session, run=run, scholar=scholar, publications=deduped_next
|
|
||||||
)
|
|
||||||
state.discovered_publication_count += discovered_count
|
|
||||||
|
|
||||||
if self._handle_page_state_transition(state=state):
|
if self._handle_page_state_transition(state=state):
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -262,25 +262,13 @@ def build_failure_debug_context(
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
def complete_run_for_user(
|
def _log_alert_threshold_warnings(
|
||||||
*,
|
*,
|
||||||
user_settings: Any,
|
|
||||||
run: CrawlRun,
|
|
||||||
scholars: list[ScholarProfile],
|
|
||||||
user_id: int,
|
user_id: int,
|
||||||
progress: RunProgress,
|
run: CrawlRun,
|
||||||
idempotency_key: str | None,
|
failure_summary: RunFailureSummary,
|
||||||
alert_blocked_failure_threshold: int,
|
alert_summary: RunAlertSummary,
|
||||||
alert_network_failure_threshold: int,
|
) -> None:
|
||||||
alert_retry_scheduled_threshold: int,
|
|
||||||
) -> tuple[RunFailureSummary, RunAlertSummary]:
|
|
||||||
failure_summary = summarize_failures(scholar_results=progress.scholar_results)
|
|
||||||
alert_summary = build_alert_summary(
|
|
||||||
failure_summary=failure_summary,
|
|
||||||
alert_blocked_failure_threshold=alert_blocked_failure_threshold,
|
|
||||||
alert_network_failure_threshold=alert_network_failure_threshold,
|
|
||||||
alert_retry_scheduled_threshold=alert_retry_scheduled_threshold,
|
|
||||||
)
|
|
||||||
if alert_summary.alert_flags["blocked_failure_threshold_exceeded"]:
|
if alert_summary.alert_flags["blocked_failure_threshold_exceeded"]:
|
||||||
structured_log(
|
structured_log(
|
||||||
logger,
|
logger,
|
||||||
|
|
@ -311,6 +299,30 @@ def complete_run_for_user(
|
||||||
retries_scheduled_count=failure_summary.retries_scheduled_count,
|
retries_scheduled_count=failure_summary.retries_scheduled_count,
|
||||||
threshold=alert_summary.retry_scheduled_threshold,
|
threshold=alert_summary.retry_scheduled_threshold,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def complete_run_for_user(
|
||||||
|
*,
|
||||||
|
user_settings: Any,
|
||||||
|
run: CrawlRun,
|
||||||
|
scholars: list[ScholarProfile],
|
||||||
|
user_id: int,
|
||||||
|
progress: RunProgress,
|
||||||
|
idempotency_key: str | None,
|
||||||
|
alert_blocked_failure_threshold: int,
|
||||||
|
alert_network_failure_threshold: int,
|
||||||
|
alert_retry_scheduled_threshold: int,
|
||||||
|
) -> tuple[RunFailureSummary, RunAlertSummary]:
|
||||||
|
failure_summary = summarize_failures(scholar_results=progress.scholar_results)
|
||||||
|
alert_summary = build_alert_summary(
|
||||||
|
failure_summary=failure_summary,
|
||||||
|
alert_blocked_failure_threshold=alert_blocked_failure_threshold,
|
||||||
|
alert_network_failure_threshold=alert_network_failure_threshold,
|
||||||
|
alert_retry_scheduled_threshold=alert_retry_scheduled_threshold,
|
||||||
|
)
|
||||||
|
_log_alert_threshold_warnings(
|
||||||
|
user_id=user_id, run=run, failure_summary=failure_summary, alert_summary=alert_summary
|
||||||
|
)
|
||||||
apply_safety_outcome(user_settings=user_settings, run=run, user_id=user_id, alert_summary=alert_summary)
|
apply_safety_outcome(user_settings=user_settings, run=run, user_id=user_id, alert_summary=alert_summary)
|
||||||
run_status = resolve_run_status(
|
run_status = resolve_run_status(
|
||||||
scholar_count=len(scholars),
|
scholar_count=len(scholars),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue