Coverage for src/secchi/api/cran.py: 94%

34 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-04 22:15 +0000

1"""CRAN adapter for R packages.""" 

2 

3from __future__ import annotations 

4 

5import httpx 

6 

7from secchi.api.sparse import SparseAdapter 

8from secchi.models import Dependency, PackageInfo, Registry, SearchResult, Version 

9 

10CRAN_DB = "https://crandb.r-pkg.org" 

11 

12 

13class CranAdapter(SparseAdapter): 

14 @property 

15 def registry(self) -> Registry: 

16 return Registry.CRAN 

17 

18 async def fetch_package(self, name: str) -> PackageInfo: 

19 async with self._client_scope() as client: 

20 response = await client.get(f"{CRAN_DB}/{name}") 

21 response.raise_for_status() 

22 data = response.json() 

23 homepage = _first_url(data.get("URL", "")) 

24 return PackageInfo( 

25 name=data.get("Package", name), 

26 registry=Registry.CRAN, 

27 description=data.get("Title", ""), 

28 author=data.get("Maintainer", ""), 

29 license=data.get("License", ""), 

30 homepage=homepage, 

31 repository_url=homepage, 

32 latest_version=data.get("Version", ""), 

33 documentation_url=f"https://cran.r-project.org/package={name}", 

34 versions=[Version(version=data.get("Version", ""))] 

35 if data.get("Version") 

36 else [], 

37 dependencies=_dependencies(data), 

38 ) 

39 

40 async def search(self, query: str, limit: int = 10) -> list[SearchResult]: 

41 try: 

42 info = await self.fetch_package(query) 

43 except (httpx.HTTPError, ValueError): 

44 return [] 

45 return [ 

46 SearchResult( 

47 name=info.name, 

48 registry=Registry.CRAN, 

49 version=info.latest_version, 

50 description=info.description, 

51 url=info.documentation_url, 

52 score=1.0, 

53 exact=info.name.casefold() == query.casefold(), 

54 ) 

55 ] 

56 

57 

58def _first_url(raw: str) -> str: 

59 return str(raw).split(",")[0].strip() 

60 

61 

62def _dependencies(data: dict) -> list[Dependency]: 

63 raw = data.get("Imports", "") 

64 if not raw: 

65 raw = data.get("Depends", "") 

66 dependencies: list[Dependency] = [] 

67 for item in str(raw).split(","): 

68 name = item.strip().split("(", 1)[0].strip() 

69 if name and name.lower() not in {"r", "r (>= 3.5.0)"}: 

70 dependencies.append(Dependency(name=name)) 

71 return dependencies