Coverage for src/secchi/services/resolver.py: 78%
27 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"""Resolve direct CLI package names into registry package references."""
3from __future__ import annotations
5from secchi.errors import ConfigError
6from secchi.models import PackageRef, Registry
7from secchi.services.search import PackageSearchService
10def parse_package_spec(spec: str, registry: str | None = None) -> PackageRef:
11 """Resolve ``name`` or ``registry:name`` with PyPI as the safe default."""
12 if ":" in spec and registry is None:
13 candidate_registry, name = spec.split(":", 1)
14 try:
15 return PackageRef(name=name, registry=Registry(candidate_registry))
16 except ValueError:
17 pass
18 try:
19 selected = Registry(registry) if registry else Registry.PYPI
20 except ValueError as exc:
21 supported = ", ".join(item.value for item in Registry)
22 raise ConfigError(
23 f"Unknown registry '{registry}'. Supported: {supported}"
24 ) from exc
25 return PackageRef(name=spec, registry=selected)
28async def resolve_package(
29 spec: str,
30 *,
31 configured_refs: list[PackageRef] | None = None,
32 registry: str | None = None,
33) -> list[PackageRef]:
34 """Resolve a package spec from config first, then exact registry matches."""
35 explicit = parse_package_spec(spec, registry)
36 if registry is not None or ":" in spec:
37 return [explicit]
39 configured = [
40 ref for ref in configured_refs or [] if ref.name.casefold() == spec.casefold()
41 ]
42 if configured:
43 return configured
45 matches = await PackageSearchService().search(spec)
46 exact = [result for result in matches if result.exact]
47 return [PackageRef(result.name, result.registry) for result in exact]