#!/usr/bin/env python3 """Generate the optional GovOPlaN developer convenience meta-package.""" from __future__ import annotations import argparse import json from pathlib import Path import re import tomllib META_ROOT = Path(__file__).resolve().parents[2] DIRECT = re.compile(r"^(govoplan-[a-z0-9-]+)(?:\[([^]]+)\])?\s+@\s+.*@v([A-Za-z0-9._+!-]+)$") LOCAL_CORE = re.compile(r"^(?:-e\s+)?\.\./govoplan-core(?:\[([^]]+)\])?$") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--workspace", type=Path, default=META_ROOT.parent) parser.add_argument( "--requirements", type=Path, default=META_ROOT / "requirements-release.txt", ) parser.add_argument( "--output", type=Path, default=META_ROOT / "packages" / "govoplan-meta" / "pyproject.toml", ) parser.add_argument("--check", action="store_true") return parser def render(*, workspace: Path, requirements: Path) -> str: core = tomllib.loads((workspace / "govoplan-core/pyproject.toml").read_text(encoding="utf-8"))["project"] version = str(core["version"]) base: list[str] = [] for raw in requirements.read_text(encoding="utf-8").splitlines(): line = raw.strip() if not line or line.startswith("#"): continue local = LOCAL_CORE.fullmatch(line) if local: extra = f"[{local.group(1)}]" if local.group(1) else "" base.append(f"govoplan-core{extra}=={version}") continue match = DIRECT.fullmatch(line) if match is None: raise ValueError(f"unsupported release requirement: {line!r}") extra = f"[{match.group(2)}]" if match.group(2) else "" base.append(f"{match.group(1)}{extra}=={match.group(3)}") base_names = {_requirement_name(item) for item in base} full: list[str] = [] for project_path in sorted(workspace.glob("govoplan-*/pyproject.toml")): project = tomllib.loads(project_path.read_text(encoding="utf-8"))["project"] name = str(project.get("name") or "") package_version = str(project.get("version") or "") if name.startswith("govoplan-") and name not in base_names: full.append(f"{name}=={package_version}") return "\n".join( [ "[build-system]", 'requires = ["setuptools>=69", "wheel"]', 'build-backend = "setuptools.build_meta"', "", "[project]", 'name = "govoplan"', f'version = {json.dumps(version)}', 'description = "Developer convenience package for a versioned GovOPlaN composition"', 'readme = "README.md"', 'requires-python = ">=3.12"', 'license = { text = "AGPL-3.0-or-later" }', "dependencies = [", *[f" {json.dumps(item)}," for item in base], "]", "", "[project.optional-dependencies]", "full = [", *[f" {json.dumps(item)}," for item in full], "]", "", "[project.urls]", 'Repository = "https://git.add-ideas.de/GovOPlaN/govoplan"', 'Documentation = "https://govoplan.add-ideas.de"', "", "[tool.setuptools.packages.find]", 'where = ["src"]', "", ] ) def _requirement_name(value: str) -> str: return value.split("[", 1)[0].split("==", 1)[0] def main() -> int: args = build_parser().parse_args() try: expected = render( workspace=args.workspace.expanduser().resolve(), requirements=args.requirements.expanduser().resolve(), ) except (OSError, KeyError, ValueError, tomllib.TOMLDecodeError) as exc: print(f"error: {exc}") return 1 output = args.output.expanduser() current = output.read_text(encoding="utf-8") if output.is_file() else None if args.check: if current != expected: print(f"error: developer meta-package is stale: {output}") return 1 print("Developer meta-package is synchronized.") return 0 output.parent.mkdir(parents=True, exist_ok=True) output.write_text(expected, encoding="utf-8") print(f"Developer meta-package written to {output}") return 0 if __name__ == "__main__": raise SystemExit(main())