from __future__ import annotations import json import tomllib from dataclasses import dataclass from pathlib import Path from urllib.parse import urlparse @dataclass(frozen=True) class RepositoryMetadata: name: str description: str | None class RepositoryMetadataExtractor: def extract(self, source_path: str | Path, url: str) -> RepositoryMetadata: root = Path(source_path) pyproject = self._from_pyproject(root) package = self._from_package_json(root) readme = self._from_readme(root) fallback_name = self._name_from_url_or_path(url) return RepositoryMetadata( name=pyproject.name or package.name or readme.name or fallback_name, description=( pyproject.description or package.description or readme.description ), ) def _from_pyproject(self, root: Path) -> RepositoryMetadata: path = root / "pyproject.toml" if not path.exists(): return RepositoryMetadata(name="", description=None) try: project = tomllib.loads(path.read_text(encoding="utf-8")).get("project", {}) except (OSError, tomllib.TOMLDecodeError): return RepositoryMetadata(name="", description=None) return RepositoryMetadata( name=str(project.get("name") or ""), description=project.get("description"), ) def _from_package_json(self, root: Path) -> RepositoryMetadata: path = root / "package.json" if not path.exists(): return RepositoryMetadata(name="", description=None) try: package = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return RepositoryMetadata(name="", description=None) return RepositoryMetadata( name=str(package.get("name") or ""), description=package.get("description"), ) def _from_readme(self, root: Path) -> RepositoryMetadata: for readme in sorted(root.glob("README*")): if not readme.is_file(): continue try: lines = readme.read_text(encoding="utf-8", errors="ignore").splitlines() except OSError: continue title = "" for line in lines: stripped = line.strip() cleaned = stripped.strip("#").strip() if stripped.startswith("#") and cleaned and not title: title = cleaned continue if cleaned: return RepositoryMetadata(name=title, description=cleaned) if title: return RepositoryMetadata(name=title, description=None) return RepositoryMetadata(name="", description=None) def _name_from_url_or_path(self, value: str) -> str: parsed = urlparse(value) path = parsed.path if parsed.scheme else value name = Path(path.rstrip("/")).name or "repository" if name.endswith(".git"): name = name[:-4] return name or "repository"