Source code for pjkm.core.groups.registry

"""Group registry: discover and load package group definitions from YAML."""

from __future__ import annotations

from pathlib import Path
from typing import Any

import yaml

from pjkm.core.models.group import PackageGroup

[docs] DEFINITIONS_DIR = Path(__file__).parent / "definitions"
# Standard search paths for custom group definitions
[docs] CUSTOM_GROUP_PATHS = [ Path.home() / ".pjkm" / "groups", Path.cwd() / ".pjkm" / "groups", ]
[docs] class GroupRegistry: """Loads and provides access to package group definitions.""" def __init__(self) -> None: self._groups: dict[str, PackageGroup] = {}
[docs] def load_builtin(self) -> None: """Load all YAML group definitions from the built-in definitions directory.""" if not DEFINITIONS_DIR.exists(): return for path in sorted(DEFINITIONS_DIR.rglob("*.yaml")): self.load_file(path)
[docs] def load_custom(self) -> list[Path]: """Load custom group definitions from ~/.pjkm/groups/ and ./.pjkm/groups/. Custom groups override built-in groups with the same ID. Returns list of directories that were loaded. """ loaded_dirs: list[Path] = [] for group_dir in CUSTOM_GROUP_PATHS: if group_dir.is_dir(): for path in sorted(group_dir.rglob("*.yaml")): self.load_file(path) loaded_dirs.append(group_dir) return loaded_dirs
[docs] def load_sources(self) -> list[tuple[str, int]]: """Load group definitions from all registered remote sources. Reads ~/.pjkm/sources.yaml and .pjkmrc.yaml group_sources, then loads cached repos. Sources must be synced first via `pjkm group source sync`. Returns list of (source_name, group_count) tuples. """ from pjkm.core.defaults import UserDefaults from pjkm.core.groups.sources import GroupSourceManager mgr = GroupSourceManager() mgr.load() # Also pull sources from .pjkmrc.yaml try: defaults = UserDefaults.load() if defaults.group_sources: mgr.load_from_defaults([s.model_dump() for s in defaults.group_sources]) except Exception: pass loaded: list[tuple[str, int]] = [] for name, groups_dir in mgr.get_all_group_dirs(): count = self.load_directory(groups_dir) if count > 0: loaded.append((name, count)) return loaded
[docs] def load_plugins(self) -> list[tuple[str, int]]: """Load groups from installed plugins via entry points.""" from importlib.metadata import entry_points loaded = [] for ep in entry_points(group="pjkm.groups"): try: groups_dir = ep.load()() if isinstance(groups_dir, Path) and groups_dir.is_dir(): count = self.load_directory(groups_dir) if count > 0: loaded.append((ep.name, count)) except Exception: pass # Skip broken plugins silently return loaded
[docs] def load_all(self) -> None: """Load all groups: built-in + custom local + remote sources + plugins.""" self.load_builtin() self.load_custom() self.load_sources() self.load_plugins()
[docs] def load_directory(self, directory: Path) -> int: """Load all YAML group definitions from an arbitrary directory. Returns count of groups loaded. """ count = 0 if not directory.is_dir(): return count for path in sorted(directory.rglob("*.yaml")): self.load_file(path) count += 1 return count
[docs] def load_file(self, path: Path) -> PackageGroup: """Load a single YAML group definition.""" with open(path) as f: data = yaml.safe_load(f) group = PackageGroup.model_validate(data) self._groups[group.id] = group return group
[docs] def get(self, group_id: str) -> PackageGroup | None: return self._groups.get(group_id)
[docs] def list_all(self) -> list[PackageGroup]: return list(self._groups.values())
[docs] def list_for_archetype(self, archetype: str) -> list[PackageGroup]: """Return groups applicable to a given archetype (empty archetypes = all).""" return [g for g in self._groups.values() if not g.archetypes or archetype in g.archetypes]
@property
[docs] def group_ids(self) -> list[str]: return list(self._groups.keys())
@staticmethod
[docs] def import_from_pyproject( pyproject_path: Path, output_dir: Path, sections: list[str] | None = None, ) -> list[Path]: """Import optional dependency groups from a pyproject.toml into YAML group files. Reads [project.optional-dependencies] from the given pyproject.toml and creates a .yaml group definition for each section (or specified sections). Returns list of created YAML files. """ import tomllib with open(pyproject_path, "rb") as f: data = tomllib.load(f) project = data.get("project", {}) opt_deps: dict[str, list[str]] = project.get("optional-dependencies", {}) if not opt_deps: return [] # Get project name for descriptions project_name = project.get("name", pyproject_path.parent.name) output_dir.mkdir(parents=True, exist_ok=True) created: list[Path] = [] for section, deps in opt_deps.items(): if sections and section not in sections: continue group_id = section.replace("-", "_") group_data: dict[str, Any] = { "id": group_id, "name": _section_to_name(section), "description": f"Imported from {project_name} [{section}]", "category": "Core Dev", "archetypes": [], "requires_groups": [], "platform_filter": None, "dependencies": {group_id: list(deps)}, "scaffolded_files": [], "pyproject_tool_config": {}, } out_path = output_dir / f"{group_id}.yaml" with open(out_path, "w") as f: yaml.dump(group_data, f, default_flow_style=False, sort_keys=False) created.append(out_path) return created
def _section_to_name(section: str) -> str: """Convert a pyproject section name to a human-readable name.""" return section.replace("-", " ").replace("_", " ").title()