Coverage for src/local_deep_research/research_library/downloaders/direct_pdf.py: 98%
60 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""
2Direct PDF Link Downloader
3"""
5from typing import Dict, Optional
6from urllib.parse import urlparse
7from loguru import logger
9from .base import BaseDownloader, ContentType, DownloadResult
10from ...security.ssrf_validator import redact_url_for_log
13class DirectPDFDownloader(BaseDownloader):
14 """Downloader for direct PDF links."""
16 def can_handle(self, url: str) -> bool:
17 """Check if URL is a direct PDF link using proper URL parsing."""
18 try:
19 # Parse the URL
20 parsed = urlparse(url.lower())
21 path = parsed.path or ""
22 query = parsed.query or ""
24 # Check for .pdf extension in path
25 if path.endswith(".pdf"):
26 return True
28 # Check for .pdf with query parameters
29 if ".pdf?" in url.lower(): 29 ↛ 30line 29 didn't jump to line 30 because the condition on line 29 was never true
30 return True
32 # Check for /pdf/ in path
33 if "/pdf/" in path:
34 return True
36 # Check query parameters for PDF format
37 if "type=pdf" in query or "format=pdf" in query:
38 return True
40 return False
42 except Exception:
43 logger.warning(f"Error parsing URL {redact_url_for_log(url)}")
44 return False
46 def download(
47 self, url: str, content_type: ContentType = ContentType.PDF
48 ) -> Optional[bytes]:
49 """Download PDF directly from URL."""
50 if content_type == ContentType.TEXT:
51 # Download PDF and extract text
52 pdf_content = self._download_pdf(url)
53 if pdf_content:
54 text = self.extract_text_from_pdf(pdf_content)
55 if text:
56 return text.encode("utf-8")
57 return None
58 return self._download_pdf(url)
60 def download_with_result(
61 self, url: str, content_type: ContentType = ContentType.PDF
62 ) -> DownloadResult:
63 """Download content and return detailed result with skip reason."""
64 if content_type == ContentType.TEXT:
65 # Download PDF and extract text
66 pdf_content = self._download_pdf(url)
67 if pdf_content:
68 text = self.extract_text_from_pdf(pdf_content)
69 if text:
70 return DownloadResult(
71 content=text.encode("utf-8"), is_success=True
72 )
73 return DownloadResult(
74 skip_reason="PDF downloaded but text extraction failed"
75 )
76 return DownloadResult(
77 skip_reason="Could not download PDF from direct link"
78 )
79 # Try to download PDF directly
80 logger.info(
81 f"Attempting direct PDF download from {redact_url_for_log(url)}"
82 )
83 pdf_content = super()._download_pdf(url)
85 if pdf_content:
86 logger.info(
87 f"Successfully downloaded PDF directly from "
88 f"{redact_url_for_log(url)}"
89 )
90 return DownloadResult(content=pdf_content, is_success=True)
91 # Try to determine specific reason for failure
92 try:
93 response = self.session.head(url, timeout=5, allow_redirects=True)
94 if response.status_code == 404:
95 return DownloadResult(
96 skip_reason="PDF file not found (404) - link may be broken",
97 status_code=404,
98 )
99 if response.status_code == 403:
100 return DownloadResult(
101 skip_reason="Access denied (403) - PDF requires authentication",
102 status_code=403,
103 )
104 if response.status_code >= 500:
105 return DownloadResult(
106 skip_reason=f"Server error ({response.status_code}) - try again later",
107 status_code=response.status_code,
108 )
109 return DownloadResult(
110 skip_reason=f"Could not download PDF - server returned status {response.status_code}",
111 status_code=response.status_code,
112 )
113 except Exception:
114 return DownloadResult(
115 skip_reason="Failed to download PDF from direct link"
116 )
118 def _download_pdf(
119 self, url: str, headers: Optional[Dict[str, str]] = None
120 ) -> Optional[bytes]:
121 """Download PDF directly from URL."""
122 logger.info(f"Downloading PDF directly from: {redact_url_for_log(url)}")
123 return super()._download_pdf(url)