agent_search_gateway.providers.academic.dblp
dblp publication search API adapter.
1"""dblp publication search API adapter.""" 2 3from __future__ import annotations 4 5import xml.etree.ElementTree as ET 6from datetime import date 7 8from ...academic.normalization import normalize_dblp_key, normalize_doi 9from ..contracts import PaperSearchHit 10from .common import AcademicHttpExecutor, protocol_failure, reject_item 11 12_DEFAULT_API_URL = "https://dblp.org/search/publ/api" 13 14 15class DblpProvider: 16 name = "dblp" 17 18 def __init__( 19 self, 20 executor: AcademicHttpExecutor, 21 *, 22 api_url: str = _DEFAULT_API_URL, 23 ) -> None: 24 self._executor = executor 25 self._api_url = api_url 26 27 async def search(self, query: str) -> list[PaperSearchHit]: 28 body = await self._executor.request_text( 29 "GET", 30 self._api_url, 31 stage="paper_search", 32 params={"q": query, "format": "xml", "h": 10}, 33 ) 34 try: 35 root = ET.fromstring(body) 36 except ET.ParseError as exc: 37 raise protocol_failure(self.name, "response was not valid XML") from exc 38 if root.tag != "result" or root.find("hits") is None: 39 raise protocol_failure(self.name, "response XML envelope was invalid") 40 hits: list[PaperSearchHit] = [] 41 for hit in root.findall("./hits/hit"): 42 mapped = self._map_hit(hit) 43 if mapped is None: 44 reject_item(self.name) 45 else: 46 hits.append(mapped) 47 return hits 48 49 def _map_hit(self, hit: ET.Element) -> PaperSearchHit | None: 50 info = hit.find("info") 51 if info is None: 52 return None 53 title_element = info.find("title") 54 title = self._element_text(title_element) 55 url = self._child_text(info, "url") 56 source_id = normalize_dblp_key((info.get("key") or "").strip()) 57 if source_id is None: 58 source_id = normalize_dblp_key(url) 59 if source_id is None or not title or not url: 60 return None 61 authors = tuple( 62 author 63 for element in info.findall("./authors/author") 64 if (author := self._element_text(element)) 65 ) 66 doi = "" 67 for edition in info.findall("ee"): 68 candidate = normalize_doi(self._element_text(edition)) 69 if candidate is not None: 70 doi = candidate 71 break 72 published_date = self._year_date(self._child_text(info, "year")) 73 return PaperSearchHit( 74 source=self.name, 75 source_id=source_id, 76 title=title, 77 authors=authors, 78 doi=doi, 79 published_date=published_date, 80 url=url, 81 venue=self._child_text(info, "venue"), 82 ) 83 84 @staticmethod 85 def _element_text(element: ET.Element | None) -> str: 86 if element is None: 87 return "" 88 return " ".join("".join(element.itertext()).split()) 89 90 @classmethod 91 def _child_text(cls, parent: ET.Element, name: str) -> str: 92 return cls._element_text(parent.find(name)) 93 94 @staticmethod 95 def _year_date(raw_year: str) -> date | None: 96 try: 97 year = int(raw_year) 98 if year < 1: 99 return None 100 return date(year, 1, 1) 101 except (TypeError, ValueError): 102 return None
class
DblpProvider:
16class DblpProvider: 17 name = "dblp" 18 19 def __init__( 20 self, 21 executor: AcademicHttpExecutor, 22 *, 23 api_url: str = _DEFAULT_API_URL, 24 ) -> None: 25 self._executor = executor 26 self._api_url = api_url 27 28 async def search(self, query: str) -> list[PaperSearchHit]: 29 body = await self._executor.request_text( 30 "GET", 31 self._api_url, 32 stage="paper_search", 33 params={"q": query, "format": "xml", "h": 10}, 34 ) 35 try: 36 root = ET.fromstring(body) 37 except ET.ParseError as exc: 38 raise protocol_failure(self.name, "response was not valid XML") from exc 39 if root.tag != "result" or root.find("hits") is None: 40 raise protocol_failure(self.name, "response XML envelope was invalid") 41 hits: list[PaperSearchHit] = [] 42 for hit in root.findall("./hits/hit"): 43 mapped = self._map_hit(hit) 44 if mapped is None: 45 reject_item(self.name) 46 else: 47 hits.append(mapped) 48 return hits 49 50 def _map_hit(self, hit: ET.Element) -> PaperSearchHit | None: 51 info = hit.find("info") 52 if info is None: 53 return None 54 title_element = info.find("title") 55 title = self._element_text(title_element) 56 url = self._child_text(info, "url") 57 source_id = normalize_dblp_key((info.get("key") or "").strip()) 58 if source_id is None: 59 source_id = normalize_dblp_key(url) 60 if source_id is None or not title or not url: 61 return None 62 authors = tuple( 63 author 64 for element in info.findall("./authors/author") 65 if (author := self._element_text(element)) 66 ) 67 doi = "" 68 for edition in info.findall("ee"): 69 candidate = normalize_doi(self._element_text(edition)) 70 if candidate is not None: 71 doi = candidate 72 break 73 published_date = self._year_date(self._child_text(info, "year")) 74 return PaperSearchHit( 75 source=self.name, 76 source_id=source_id, 77 title=title, 78 authors=authors, 79 doi=doi, 80 published_date=published_date, 81 url=url, 82 venue=self._child_text(info, "venue"), 83 ) 84 85 @staticmethod 86 def _element_text(element: ET.Element | None) -> str: 87 if element is None: 88 return "" 89 return " ".join("".join(element.itertext()).split()) 90 91 @classmethod 92 def _child_text(cls, parent: ET.Element, name: str) -> str: 93 return cls._element_text(parent.find(name)) 94 95 @staticmethod 96 def _year_date(raw_year: str) -> date | None: 97 try: 98 year = int(raw_year) 99 if year < 1: 100 return None 101 return date(year, 1, 1) 102 except (TypeError, ValueError): 103 return None
DblpProvider( executor: agent_search_gateway.providers.academic.common.AcademicHttpExecutor, *, api_url: str = 'https://dblp.org/search/publ/api')
async def
search( self, query: str) -> list[agent_search_gateway.providers.contracts.PaperSearchHit]:
28 async def search(self, query: str) -> list[PaperSearchHit]: 29 body = await self._executor.request_text( 30 "GET", 31 self._api_url, 32 stage="paper_search", 33 params={"q": query, "format": "xml", "h": 10}, 34 ) 35 try: 36 root = ET.fromstring(body) 37 except ET.ParseError as exc: 38 raise protocol_failure(self.name, "response was not valid XML") from exc 39 if root.tag != "result" or root.find("hits") is None: 40 raise protocol_failure(self.name, "response XML envelope was invalid") 41 hits: list[PaperSearchHit] = [] 42 for hit in root.findall("./hits/hit"): 43 mapped = self._map_hit(hit) 44 if mapped is None: 45 reject_item(self.name) 46 else: 47 hits.append(mapped) 48 return hits