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
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-04 22:15 +0000
1"""CRAN adapter for R packages."""
3from __future__ import annotations
5import httpx
7from secchi.api.sparse import SparseAdapter
8from secchi.models import Dependency, PackageInfo, Registry, SearchResult, Version
10CRAN_DB = "https://crandb.r-pkg.org"
13class CranAdapter(SparseAdapter):
14 @property
15 def registry(self) -> Registry:
16 return Registry.CRAN
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 )
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 ]
58def _first_url(raw: str) -> str:
59 return str(raw).split(",")[0].strip()
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