"""Hostname resolver: zeroconf when available, stdlib multicast fallback. Public API: resolve(host, family="v4"|"v6", timeout=1.0, retries=2, use_cache=True) -> str | None """ from . import cache from . import _stdlib_resolve as _std from ._pick import pick_best def _try_zeroconf(host: str, family: str, timeout: float) -> tuple[str, int] | None: try: from zeroconf import Zeroconf, AddressResolver, IPVersion except ImportError: return None hostname = host if host.endswith(".") else host + "." ver = IPVersion.V6Only if family == "v6" else IPVersion.V4Only zc = Zeroconf(ip_version=IPVersion.All) try: r = AddressResolver(hostname) # request() blocks up to `timeout` milliseconds if not r.request(zc, int(timeout * 1000)): return None addrs = r.parsed_addresses(ver) best = pick_best(addrs, family=family) if not best: return None return best, 120 except Exception: return None finally: try: zc.close() except Exception: pass def resolve(host: str, family: str = "v4", timeout: float = 1.0, retries: int = 2, use_cache: bool = True) -> str | None: hostname = host if host.endswith(".local") else f"{host}.local" if use_cache: hit = cache.get(hostname, family) if hit: return hit per_try = max(0.3, timeout / max(1, retries)) # IPv4: try stdlib first (fast, no zeroconf startup cost). if family == "v4": for _ in range(max(1, retries)): recs = _std.query_all(hostname, per_try) if recs: best = pick_best([ip for ip, _ in recs], family="v4") ttl = next((t for ip, t in recs if ip == best), 120) cache.put(hostname, best, family=family, ttl=ttl or 120) return best # IPv6 or IPv4-stdlib-miss: try zeroconf (heavier but RFC-complete). for _ in range(max(1, retries)): res = _try_zeroconf(hostname, family, per_try) if res: ip, ttl = res cache.put(hostname, ip, family=family, ttl=ttl) return ip return None