Coverage for src/local_deep_research/research_library/downloaders/biorxiv.py: 98%
98 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"""
2bioRxiv/medRxiv PDF and Text Downloader
3"""
5import re
6from typing import Dict, Optional
7from urllib.parse import urlparse
9import requests
10from loguru import logger
12from ...security.ssrf_validator import redact_url_for_log
13from ...security.egress.policy import PolicyDeniedError
14from .base import BaseDownloader, ContentType, DownloadResult
17class BioRxivDownloader(BaseDownloader):
18 """Downloader for bioRxiv and medRxiv preprints."""
20 def can_handle(self, url: str) -> bool:
21 """Check if URL is from bioRxiv or medRxiv."""
22 try:
23 hostname = urlparse(url).hostname
24 if not hostname:
25 return False
26 return (
27 hostname == "biorxiv.org"
28 or hostname.endswith(".biorxiv.org")
29 or hostname == "medrxiv.org"
30 or hostname.endswith(".medrxiv.org")
31 )
32 except Exception:
33 return False
35 def download(
36 self, url: str, content_type: ContentType = ContentType.PDF
37 ) -> Optional[bytes]:
38 """Download content from bioRxiv/medRxiv."""
39 if content_type == ContentType.TEXT:
40 return self._download_text(url)
41 return self._download_pdf(url)
43 def download_with_result(
44 self, url: str, content_type: ContentType = ContentType.PDF
45 ) -> DownloadResult:
46 """Download content and return detailed result with skip reason."""
47 if content_type == ContentType.TEXT:
48 # Try to get text from page
49 text = self._fetch_abstract_from_page(url)
50 if text:
51 return DownloadResult(
52 content=text.encode("utf-8"),
53 is_success=True,
54 content_is_complete=False,
55 )
57 # Fallback to PDF extraction
58 pdf_content = self._download_pdf(url)
59 if pdf_content:
60 extracted_text = self.extract_text_from_pdf(pdf_content)
61 if extracted_text:
62 return DownloadResult(
63 content=extracted_text.encode("utf-8"), is_success=True
64 )
66 return DownloadResult(
67 skip_reason="Could not extract text from bioRxiv/medRxiv article"
68 )
69 # Try to download PDF
70 pdf_url = self._convert_to_pdf_url(url)
71 if not pdf_url:
72 return DownloadResult(
73 skip_reason="Invalid bioRxiv/medRxiv URL format"
74 )
76 logger.info(
77 f"Downloading bioRxiv/medRxiv PDF from {redact_url_for_log(pdf_url)}"
78 )
79 pdf_content = super()._download_pdf(pdf_url)
81 if pdf_content:
82 return DownloadResult(content=pdf_content, is_success=True)
83 # Check if it's a server issue or article doesn't exist
84 try:
85 response = self.session.head(url, timeout=5)
86 if response.status_code == 404:
87 return DownloadResult(
88 skip_reason="Article not found on bioRxiv/medRxiv",
89 status_code=404,
90 )
91 if response.status_code >= 500:
92 return DownloadResult(
93 skip_reason="bioRxiv/medRxiv server temporarily unavailable",
94 status_code=response.status_code,
95 )
96 except requests.RequestException:
97 logger.opt(exception=False).debug(
98 "Failed to check bioRxiv/medRxiv URL: {}",
99 redact_url_for_log(url),
100 )
101 return DownloadResult(
102 skip_reason="Failed to download PDF from bioRxiv/medRxiv"
103 )
105 def _download_pdf(
106 self, url: str, headers: Optional[Dict[str, str]] = None
107 ) -> Optional[bytes]:
108 """Download PDF from bioRxiv/medRxiv."""
109 # Convert URL to PDF format
110 pdf_url = self._convert_to_pdf_url(url)
112 if not pdf_url:
113 logger.error(
114 f"Could not convert to PDF URL: {redact_url_for_log(url)}"
115 )
116 return None
118 logger.info(
119 f"Downloading bioRxiv/medRxiv PDF from {redact_url_for_log(pdf_url)}"
120 )
121 return super()._download_pdf(pdf_url)
123 def _download_text(self, url: str) -> Optional[bytes]:
124 """Get text content from bioRxiv/medRxiv."""
125 # Try to get abstract and metadata from the HTML page
126 text = self._fetch_abstract_from_page(url)
127 if text:
128 return text.encode("utf-8")
130 # Fallback: Download PDF and extract text
131 pdf_content = self._download_pdf(url)
132 if pdf_content:
133 extracted_text = self.extract_text_from_pdf(pdf_content)
134 if extracted_text:
135 return extracted_text.encode("utf-8")
137 return None
139 def _convert_to_pdf_url(self, url: str) -> Optional[str]:
140 """Convert bioRxiv/medRxiv URL to PDF URL."""
141 # Handle different URL patterns
142 # Example: https://www.biorxiv.org/content/10.1101/2024.01.01.123456v1
143 # Becomes: https://www.biorxiv.org/content/10.1101/2024.01.01.123456v1.full.pdf
145 # Remove any existing .full or .full.pdf
146 base_url = re.sub(r"\.full(\.pdf)?$", "", url)
148 # Check if it's already a PDF URL
149 if base_url.endswith(".pdf"):
150 return base_url
152 # Add .full.pdf
153 pdf_url = base_url.rstrip("/") + ".full.pdf"
155 # Handle content vs. content/early URLs
156 return pdf_url.replace("/content/early/", "/content/")
158 def _fetch_abstract_from_page(self, url: str) -> Optional[str]:
159 """Fetch abstract and metadata from bioRxiv/medRxiv page."""
160 try:
161 # Request the HTML page
162 response = self.session.get(url, timeout=10)
164 if response.status_code == 200:
165 # Simple extraction using regex (avoiding BeautifulSoup dependency)
166 html = response.text
167 text_parts = []
169 # Extract title
170 title_match = re.search(
171 r'<meta\s+name="DC\.Title"\s+content="([^"]+)"', html
172 )
173 if title_match:
174 text_parts.append(f"Title: {title_match.group(1)}")
176 # Extract authors
177 author_match = re.search(
178 r'<meta\s+name="DC\.Creator"\s+content="([^"]+)"', html
179 )
180 if author_match:
181 text_parts.append(f"Authors: {author_match.group(1)}")
183 # Extract abstract
184 abstract_match = re.search(
185 r'<meta\s+name="DC\.Description"\s+content="([^"]+)"',
186 html,
187 re.DOTALL,
188 )
189 if abstract_match:
190 abstract = abstract_match.group(1)
191 # Clean up HTML entities
192 abstract = abstract.replace("<", "<").replace(
193 ">", ">"
194 )
195 abstract = abstract.replace(""", '"').replace(
196 "'", "'"
197 )
198 abstract = abstract.replace("&", "&")
199 text_parts.append(f"\nAbstract:\n{abstract}")
201 if text_parts:
202 logger.info(
203 "Retrieved text content from bioRxiv/medRxiv page"
204 )
205 return "\n".join(text_parts)
207 except PolicyDeniedError:
208 raise
209 except Exception as e:
210 logger.opt(exception=False).debug(
211 "Failed to fetch abstract from bioRxiv/medRxiv: {} ({})",
212 redact_url_for_log(url),
213 type(e).__name__,
214 )
216 return None